Flutter’s Navigator 2.0 is a significant update to the navigation system, offering more control, flexibility, and predictability over routing and navigation in Flutter applications. Unlike the original Navigator, Navigator 2.0 exposes the underlying navigation stack, allowing developers to manage it programmatically. This provides more fine-grained control, making complex navigation scenarios, such as deep linking, easier to handle.
What is Navigator 2.0?
Navigator 2.0 introduces an imperative navigation API, replacing the declarative approach of Navigator 1.0. This means instead of relying on push
and pop
methods, developers can now directly manipulate the navigation stack. Key components of Navigator 2.0 include:
RouterDelegate
: Manages the app’s navigation state and builds the navigator.RouteInformationParser
: Converts aRouteInformation
object (usually from the URL) into a user-defined data type that theRouterDelegate
can understand.RouteInformationProvider
: Provides route information to the router. The default implementation isPlatformRouteInformationProvider
, which gets the initial route from the platform.BackButtonDispatcher
: Dispatches back button presses to the active route.
Why Use Navigator 2.0?
Navigator 2.0 is particularly beneficial for apps requiring:
- Deep Linking: Handling navigation from external links.
- Complex Navigation: Apps with intricate navigation patterns that are hard to manage with the original Navigator.
- Web Support: Building Flutter apps that run in a browser, where URL management is critical.
- Custom Navigation Logic: Implementing advanced routing strategies.
How to Implement Navigator 2.0
Implementing Navigator 2.0 involves several steps, from setting up the RouterDelegate
to handling route information.
Step 1: Create a Custom Route Information Parser
The RouteInformationParser
converts the RouteInformation
(e.g., URL from a web browser) into a data type understood by your RouterDelegate
.
import 'package:flutter/widgets.dart';
class MyRouteInformationParser extends RouteInformationParser<RouteSettings> {
@override
Future<RouteSettings> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.uri.toString());
// Handle initial/unknown routes
if (uri.pathSegments.isEmpty) {
return const RouteSettings(name: '/home');
}
// Parse the route and return RouteSettings
return RouteSettings(name: '/${uri.pathSegments.first}');
}
@override
RouteInformation? restoreRouteInformation(RouteSettings configuration) {
if (configuration.name == null) {
return const RouteInformation(uri: Uri.parse('/home'));
}
return RouteInformation(uri: Uri.parse(configuration.name!));
}
}
In this example:
parseRouteInformation
takes aRouteInformation
object and parses it into aRouteSettings
.restoreRouteInformation
converts theRouteSettings
back into aRouteInformation
. This is used when the app needs to update the browser URL.
Step 2: Implement the Router Delegate
The RouterDelegate
is responsible for building the navigator and managing the app’s navigation stack. It listens to changes and rebuilds the UI accordingly.
import 'package:flutter/material.dart';
class MyRouterDelegate extends RouterDelegate<RouteSettings>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteSettings> {
RouteSettings _currentConfiguration = const RouteSettings(name: '/home');
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
RouteSettings get currentConfiguration => _currentConfiguration;
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (_currentConfiguration.name == '/home')
const MaterialPage(
key: ValueKey('HomePage'),
child: HomePage(),
)
else if (_currentConfiguration.name == '/details')
const MaterialPage(
key: ValueKey('DetailsPage'),
child: DetailsPage(),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
_currentConfiguration = const RouteSettings(name: '/home');
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(RouteSettings configuration) async {
_currentConfiguration = configuration;
notifyListeners();
}
void goToDetails() {
_currentConfiguration = const RouteSettings(name: '/details');
notifyListeners();
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
child: const Text('Go to Details'),
onPressed: () {
MyRouterDelegate delegate =
Router.of(context).routerDelegate as MyRouterDelegate;
delegate.goToDetails();
},
),
),
);
}
}
class DetailsPage extends StatelessWidget {
const DetailsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: const Center(
child: Text('Details Page'),
),
);
}
}
Key components of the RouterDelegate
:
navigatorKey
: A global key for theNavigator
.currentConfiguration
: Holds the current route settings.build
: Constructs theNavigator
based on thecurrentConfiguration
. It uses a list ofPage
widgets to define the navigation stack.setNewRoutePath
: Updates thecurrentConfiguration
based on the new route. This is called when the browser URL changes (e.g., deep linking).onPopPage
: Handles popping pages from the navigator stack.
Step 3: Configure the App Widget
Wrap your top-level widget with the Router
widget, providing instances of your custom RouterDelegate
and RouteInformationParser
.
import 'package:flutter/material.dart';
import 'package:navigator_2_example/route_information_parser.dart';
import 'package:navigator_2_example/router_delegate.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
MyRouterDelegate _routerDelegate = MyRouterDelegate();
MyRouteInformationParser _routeInformationParser = MyRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Navigator 2.0 Example',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
backButtonDispatcher: RootBackButtonDispatcher(),
);
}
}
Here, the MaterialApp.router
is configured with:
routerDelegate
: An instance ofMyRouterDelegate
.routeInformationParser
: An instance ofMyRouteInformationParser
.backButtonDispatcher
: Manages the back button presses.
Complete Example
Below is the complete runnable code that combines all the steps described above. You can directly use it in your Flutter project.
// main.dart
import 'package:flutter/material.dart';
import 'route_information_parser.dart';
import 'router_delegate.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final MyRouterDelegate _routerDelegate = MyRouterDelegate();
final MyRouteInformationParser _routeInformationParser =
MyRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Navigator 2.0 Example',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
backButtonDispatcher: RootBackButtonDispatcher(),
);
}
}
// route_information_parser.dart
import 'package:flutter/widgets.dart';
class MyRouteInformationParser extends RouteInformationParser<RouteSettings> {
@override
Future<RouteSettings> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.uri.toString());
if (uri.pathSegments.isEmpty) {
return const RouteSettings(name: '/home');
}
return RouteSettings(name: '/${uri.pathSegments.first}');
}
@override
RouteInformation? restoreRouteInformation(RouteSettings configuration) {
if (configuration.name == null) {
return const RouteInformation(uri: Uri.parse('/home'));
}
return RouteInformation(uri: Uri.parse(configuration.name!));
}
}
// router_delegate.dart
import 'package:flutter/material.dart';
class MyRouterDelegate extends RouterDelegate<RouteSettings>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteSettings> {
RouteSettings _currentConfiguration = const RouteSettings(name: '/home');
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
RouteSettings get currentConfiguration => _currentConfiguration;
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (_currentConfiguration.name == '/home')
const MaterialPage(
key: ValueKey('HomePage'),
child: HomePage(),
)
else if (_currentConfiguration.name == '/details')
const MaterialPage(
key: ValueKey('DetailsPage'),
child: DetailsPage(),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
_currentConfiguration = const RouteSettings(name: '/home');
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(RouteSettings configuration) async {
_currentConfiguration = configuration;
notifyListeners();
}
void goToDetails() {
_currentConfiguration = const RouteSettings(name: '/details');
notifyListeners();
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
child: const Text('Go to Details'),
onPressed: () {
MyRouterDelegate delegate =
Router.of(context).routerDelegate as MyRouterDelegate;
delegate.goToDetails();
},
),
),
);
}
}
class DetailsPage extends StatelessWidget {
const DetailsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: const Center(
child: Text('Details Page'),
),
);
}
}
Conclusion
Navigator 2.0 provides Flutter developers with the tools needed for complex navigation scenarios, including deep linking and web support. While it requires a deeper understanding and more code compared to the original Navigator, the flexibility and control it offers make it invaluable for sophisticated applications. By implementing custom RouteInformationParser
and RouterDelegate
, you can manage navigation in a way that perfectly fits your application’s needs.