Flutter provides multiple ways to handle navigation between screens. While Navigator 1.0 (imperative navigation) is widely used for small apps, complex applications benefit from Navigator 2.0, which introduces declarative navigation. Navigator 2.0 gives developers full control over the navigation stack, supports deep linking, and allows a more predictable routing mechanism. This post provides an in-depth guide to Navigator 2.0, including its basics, architecture, examples, and best practices.
Understanding Navigation in Flutter
Navigation is the process of moving between screens in a mobile application. Flutter originally used a stack-based navigation system through the Navigator widget:
- Navigator 1.0: Imperative navigation using
pushandpop. - Navigator 2.0: Declarative navigation where you describe the state of the navigation stack, and the framework renders it.
Navigator 2.0 is ideal for:
- Apps with deep linking.
- Complex navigation flows with nested routers.
- Applications that require state synchronization between URL, app state, and navigation stack.
Imperative vs Declarative Navigation
Imperative Navigation (Navigator 1.0)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ProfileScreen()),
);
- You explicitly tell Flutter when and what to push on the navigation stack.
- Works well for simple apps with few screens.
Declarative Navigation (Navigator 2.0)
Navigator(
pages: [
MaterialPage(child: HomeScreen()),
if (showProfile) MaterialPage(child: ProfileScreen()),
],
onPopPage: (route, result) => route.didPop(result),
);
- You describe the current state of the app’s navigation.
- The navigation stack updates automatically to match the state.
- Easier to manage deep linking, back button handling, and state restoration.
Key Concepts of Navigator 2.0
Navigator 2.0 introduces several new concepts:
- Router: A widget that manages a navigation stack.
- RouteInformationParser: Converts a URL or route string into a configuration object.
- RouterDelegate: Builds the Navigator based on the app’s current state.
- BackButtonDispatcher: Handles system back button presses, especially for web and Android.
- Page: Represents a single screen in the navigation stack.
This architecture allows declarative navigation and better integration with URLs and state.
Setting Up Navigator 2.0
Step 1: Define App State
First, define a class that represents the current navigation state:
class AppState extends ChangeNotifier {
bool showProfile = false;
void goToProfile() {
showProfile = true;
notifyListeners();
}
void goHome() {
showProfile = false;
notifyListeners();
}
}
- The app state controls which screens should be visible.
notifyListeners()triggers updates in the navigation stack.
Step 2: Create a RouterDelegate
RouterDelegate builds the Navigator based on the app state:
class MyRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey<NavigatorState> navigatorKey;
final AppState appState;
MyRouterDelegate(this.appState) : navigatorKey = GlobalKey<NavigatorState>() {
appState.addListener(notifyListeners);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(child: HomeScreen(onProfile: appState.goToProfile)),
if (appState.showProfile)
MaterialPage(child: ProfileScreen(onBack: appState.goHome)),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
appState.goHome();
return true;
},
);
}
@override
Future<void> setNewRoutePath(configuration) async {}
}
pagesis a list of all screens currently visible.- The stack updates automatically when the app state changes.
onPopPagehandles back button navigation.
Step 3: Create a RouteInformationParser
RouteInformationParser converts URLs into a typed configuration object:
class MyRouteParser extends RouteInformationParser<AppState> {
@override
Future<AppState> parseRouteInformation(RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location!);
final appState = AppState();
if (uri.pathSegments.contains('profile')) {
appState.showProfile = true;
}
return appState;
}
@override
RouteInformation? restoreRouteInformation(AppState configuration) {
if (configuration.showProfile) {
return RouteInformation(location: '/profile');
}
return RouteInformation(location: '/');
}
}
parseRouteInformationis called when the URL changes (web or deep link).restoreRouteInformationconverts the app state back into a URL.
Step 4: Integrate Router in MaterialApp
final appState = AppState();
MaterialApp.router(
routerDelegate: MyRouterDelegate(appState),
routeInformationParser: MyRouteParser(),
);
MaterialApp.routerreplaces the traditionalMaterialAppwhen using Navigator 2.0.- All navigation is now declarative and controlled by app state.
Benefits of Navigator 2.0
- Deep Linking: URLs directly map to app state and screens.
- State-Driven Navigation: The navigation stack is a reflection of the app state.
- Better Web Support: Works seamlessly with browser back and forward buttons.
- Predictable Stack: Navigation is always consistent with the app’s current state.
- Custom Transitions: Full control over page animations using the
pageslist.
Handling Back Button with Navigator 2.0
Back button presses (system or web) can be managed declaratively:
- Implement
onPopPageinRouterDelegate. - Update the app state in response to back button events.
- Use
BackButtonDispatcherfor advanced nested navigation.
@override
Future<bool> popRoute() {
if (appState.showProfile) {
appState.goHome();
return Future.value(true);
}
return Future.value(false);
}
- Returns
trueif the pop was handled. - Prevents accidental app exit when back is pressed.
Passing Data Between Screens
Navigator 2.0 encourages state-driven data passing:
class ProfileScreen extends StatelessWidget {
final String username;
final VoidCallback onBack;
ProfileScreen({this.username = 'Guest', required this.onBack});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Column(
children: [
Text('Username: $username'),
ElevatedButton(
onPressed: onBack,
child: Text('Back'),
),
],
),
);
}
}
- Data flows through the app state instead of directly via
Navigator.push. - Simplifies testing and state synchronization.
Deep Linking and URL Synchronization
Navigator 2.0 integrates seamlessly with deep linking:
- URLs reflect the current screen state.
- App state updates automatically when URLs change.
- Example:
/profileopens the profile screen,/opens home.
final uri = Uri.parse(routeInformation.location!);
if (uri.pathSegments.contains('profile')) {
appState.showProfile = true;
}
- Enables SEO-friendly web apps and consistent browser history.
Nested Navigation with Navigator 2.0
For apps with tabs or nested flows:
- Each tab can have its own RouterDelegate and Navigator.
- Each navigator manages a subset of screens independently.
- Back button handling is coordinated across multiple navigators.
class TabRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
// Manages only one tab's navigation stack
}
- Improves modularity and maintainability in large apps.
Testing Navigator 2.0
Declarative navigation makes testing easier:
testWidgets('Navigate to ProfileScreen', (tester) async {
final appState = AppState();
await tester.pumpWidget(MaterialApp.router(
routerDelegate: MyRouterDelegate(appState),
routeInformationParser: MyRouteParser(),
));
appState.goToProfile();
await tester.pumpAndSettle();
expect(find.text('Profile'), findsOneWidget);
});
- State changes drive navigation.
- No need to mock or imperatively push routes.
Best Practices for Navigator 2.0
- Keep App State Single Source of Truth: The navigation stack should always reflect the app state.
- Use Typed Configuration Objects: Avoid passing generic maps between screens.
- Implement Fallback Screens: Always handle unknown routes gracefully.
- Synchronize URLs: Essential for web apps to ensure correct browser history.
- Modularize Nested Routers: Use separate RouterDelegates for independent navigation flows.
- Test State Changes: Navigation tests should simulate app state updates rather than pushing routes imperatively.
Common Mistakes
- Mixing Navigator 1.0 and 2.0: Can cause inconsistencies in the back stack.
- Not Handling Pop Properly: Failing to update app state leads to mismatched UI and navigation stack.
- Overcomplicating RouterDelegate: Keep logic simple; use helper classes for complex scenarios.
- Ignoring URL Synchronization: Important for web apps and deep linking.
- Not Using ChangeNotifier or Equivalent: The navigator must rebuild when state changes.
Leave a Reply