Transitioning from the Flutter Navigator API to the new Router API can greatly enhance navigation flexibility and control, especially for complex cases. This guide will help you navigate the switch.
The new Router APIs aren’t breaking changes; they simply introduce a declarative approach. Before Navigator 2.0, pushing or popping multiple pages or removing one beneath the current was challenging. If you’re content with the current Navigator, you can continue using it as before.
Flutter’s Navigator 2.0 addresses the limitations of Navigator 1.0, which relied on an imperative style that could be cumbersome for dynamic and nested navigation. Navigator 2.0’s declarative approach centralizes the navigation state, simplifying complex tasks like deep linking, web integration, and state restoration, offering a more robust and flexible navigation solution.
Overview of Navigator vs. Router
Navigator API
- The traditional way of managing navigation in Flutter.
- It uses a stack-based approach (Navigator.push, Navigator.pop).
Router API
- Introduced to handle more complex navigation scenarios.
- Separates the route information from the widget tree.
- Uses Router, RouterDelegate, RouteInformationParser, and BackButtonDispatcher.
Key Components of the Router API
- Router: The main widget that manages the router state.
- RouterDelegate: Defines the navigation logic and widget building.
- RouteInformationParser: Converts route information to a data mode Tl.
- BackButtonDispatcher: Handles back button actions.
Steps to Migrate to the Router API
- Create a RouteInformationParser
- This class parses the route information into a configuration object.
- Create a RouterDelegate
- This class manages the state of the router and builds the corresponding widgets.
- Use the Router Widget
- Integrate the Router widget in the app.
Let’s learn with a practical example where we are going to develop a small project having a home page, a products page, and under a product page showing details and an about us page. Let’s demonstrate this using flutter navigator 2.0 ie. Router Api. So first of all we have to write all the key components of router Api:
Router<T> Api
The Router handles routes from the underlying platform, displaying the correct pages. In this article, it’s set up to parse browser URLs to show the right page.
More specifically, the Router listens for route information from the operating system, whether it’s an initial route at app startup, a new route from an intent, or a back press notification. The Router then parses this info into a defined type T, converts it into page objects, and passes them to the navigator widget.
T represents the type of configuration object the Router uses to determine the current route and manage navigation. Let’s start by defining the configuration object MyRoutePaths:
class MyRoutePaths {
final String? id;
final bool isUnknown;
final String pathName;
MyRoutePaths.products()
: id = null, isUnknown = false, pathName = 'productsPage';
MyRoutePaths.unknown()
: id = null, isUnknown = true, pathName = 'unknownPage';
}
Key components:
RouteInformationProvider
The RouteInformationProvider is responsible for providing route information to the application. This includes the current location or route information, which can be parsed and used to determine the state of the navigation stack. The route information typically includes a URI or path that represents the current location in the app.
RouteInformationParser<T>
The RouteInformationParser is responsible for converting the route information into a state object that the RouterDelegate can use. It parses the route information (usually a URI) and converts it into a configuration or state object that describes the current navigation state. This parsed state is then used by the RouterDelegate to build the appropriate navigation stack.
This class has two methods that we can override to change how names obtained from the routeInformationProvider are interpreted. It must implement the RouteInformationParser interface, specialized with the same type as the Router itself. This type, T, represents the data type that the routeInformationParser will generate.
parseRouteInformation:
- This method takes a RouteInformation object (which contains the URL location) and converts it into a custom MyRoutePath object.
- It is asynchronous, allowing for complex parsing operations if needed.
@override
Future parseRouteInformation(RouteInformation routeInformation) async{
if (routeInformation.uri.pathSegments.length==1) {
if(routeInformation.uri.pathSegments[0]=='products') {
return MyRoutePaths.products();
}
}
return MyRoutePaths.unknown();
}
restoreRouteInformation:
- This method takes the custom MyRoutePath object and converts it back into a RouteInformation object.
- This is used when the app needs to update the URL to reflect the current navigation state.
@override
RouteInformation? restoreRouteInformation(MyRoutePaths configuration) {
if(configuration.id == null) {
if(configuration.pathName == 'productsPage') {
return RouteInformation(uri: Uri.parse('/products'));
}
}
return RouteInformation(uri);
}
}
RouterDelegate<T>
The RouterDelegate is a key component that controls the navigation state and the widget tree that corresponds to that state. It is responsible for building the navigation stack based on the current state and handling navigation events such as pushing or popping routes. The RouterDelegate interacts with the RouteInformationProvider to update the navigation state based on route information changes.
RouterDelegate consists of several key methods or components that performs together to operate router delegation:
navigatorKey:
- A key that provides access to the navigator state. This is required to manage the navigator’s state.
@override
GlobalKey? get navigatorKey => GlobalKey();
currentConfiguration:
- This getter returns the current route configuration. It tells the router what the current state of navigation is.
class MyRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
String? selectedItemId;
String? pathName;
bool show404 = false;
@override
MyRoutePaths? get currentConfiguration {
if (show404) {
return MyRoutePaths.unknown();
}
if(selectedItemId == null) {
if(pathName == 'productsPage') {
return MyRoutePaths.products();
}
return MyRoutePaths.home();
}
}
setNewRoutePath:
This method is called when the router updates its route based on a new path. It sets the internal state based on the incoming MyRoutePath and calls notifyListeners to rebuild the navigator.
class MyRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
String? selectedItemId;
String? pathName;
bool show404 = false;
@override
Future setNewRoutePath(MyRoutePaths configuration) async{
if (configuration.isUnknown) {
show404 = true;
notifyListeners();
return;
}
if(configuration.id == null) {
selectedItemId = null;
pathName = configuration.pathName;
}
show404 = false;
notifyListeners();
}
}
build:
- This method builds the navigator and defines the pages that should be displayed based on the current state.
class MyRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
String? selectedItemId;
String? pathName;
bool show404 = false;
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('HomePage'),
child: HomePage(onTapped: _handleOnTap,)
),
if (show404) MaterialPage(key: ValueKey('Unknown Page'), child: UnknownPage())
if(pathName == 'productsPage')
MaterialPage(
key: ValueKey('ProductsPage'),
child: ProductsPage(onProductSelected: _handleOnProductSelection,)
)
],
}
onPopPage:
- This method handles the back button press and other pop actions. It updates the navigation state accordingly. This is a property of Navigator widget.
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
if (pathName == 'productPage') {
pathName = 'productsPage';
selectedItemId = null;
notifyListeners();
}
return true;
},
);
}
BackButtonDispatcher
The BackButtonDispatcher manages back button presses and other navigation events, allowing the app to intercept and control back button behavior. It ensures these events are properly routed to the RouterDelegate or other navigation components. This is especially useful for web apps or custom back navigation scenarios. The root router usually uses a RootBackButtonDispatcher, which listens to popRoute notifications via WidgetsBindingObserver from SystemChannels.navigation. Nested routers typically use a ChildBackButtonDispatcher, provided by the ancestor router’s BackButtonDispatcher (accessible through Router.of).
Using in our application:
Now use this router api in our application using router named constructor of `MaterialApp` class as below:
void main() {
setPathUrlStrategy();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
routeInformationParser: MyRouteInfoParser(),
routerDelegate: MyRouterDelegate(),
);
}
}
RoutingConfig
In the context of Navigator 2.0, routingConfig typically refers to the configuration of routes and their corresponding screens or widgets. It includes the setup of route paths, their parameters, and the associated widgets that should be displayed for each route. The routingConfig helps define the overall navigation structure of the app and ensures that the correct widgets are displayed based on the current navigation state. It simplifies the configuration of routing by bundling all the necessary components (RouteInformationParser, RouterDelegate, RouteInformationProvider, and BackButtonDispatcher) into a single object like as:
final myRouterConfig = RouterConfig(
routeInformationProvider: PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(uri: Uri.parse('/')),
),
routeInformationParser: MyRouteInfoParser(),
routerDelegate: MyRouterDelegate(),
backButtonDispatcher: RootBackButtonDispatcher());
Then using in our app as:
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
routerConfig: myRouterConfig,
);
}
Relationship Between Components
- RouteInformationProvider provides the current route information (e.g., a URI or path).
- RouteInformationParser parses this route information into a configuration or state object.
- RouterDelegate uses the parsed state to build and manage the navigation stack. Based on the current state, it determines which widgets to display.
- BackButtonDispatcher handles back navigation events and ensures that they are appropriately routed to the RouterDelegate or other parts of the navigation system.
- RoutingConfig defines the overall structure and mapping of routes to widgets, guiding the RouterDelegate in building the navigation stack based on the parsed route information.
Together, these components provide a robust and flexible system for managing navigation in Flutter applications. They allow for dynamic and complex navigation structures that can respond to changes in route information and user interactions.
We can use this from view as:
class HomePage extends StatelessWidget {
const HomePage({
super.key,
required this.onTapped,
});
final Function onTapped;
@override
Widget build(BuildContext context) {
return DefaultWrapper(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Home Page'),
TextButton(
onPressed: (){
onTapped('productsPage');
},
child: Text('Go to products page')
),
],
),
)
);
}
}
You can get the full project source code from github via this link:
https://github.com/Ahsan-dev/navigator_two.git
Benefits of the Router API
- URL Sync: Keep your app’s state in sync with the URL, crucial for web apps.
- Better UX: Ensure seamless navigation with smooth back button handling and deep linking.
- Enhanced Flexibility: Easily manage complex routes and dynamic content.
Though the Router API may seem complex at first, it quickly becomes a powerful tool in Flutter development. Ready to simplify the boilerplate? In our next installment, we’ll introduce a package that makes declarative routing easy.
Conclusion
Upgrading from the Navigator API to the Router API in Flutter maximizes your app’s navigation potential. While the Navigator API suits simpler apps, the Router API offers the control and flexibility essential for complex navigation, especially in web apps. With its declarative style, the Router API simplifies dynamic URL handling, deep linking, and browser history management. Mastering RoutePath, RouteInformationParser, and RouterDelegate enables you to create a robust and scalable navigation system.
Happy coding!