Navigation & Routing
Navigation and routing are fundamental aspects of any mobile application, defining how users move between screens and how the app maintains its navigation state. Flutter offers multiple approaches to handle navigation, each with its own strengths and use cases.
Navigation Approaches in Flutter
Section titled “Navigation Approaches in Flutter”Flutter provides several ways to handle navigation and routing, from simple imperative APIs to more complex declarative approaches:
Navigator 1.0: The original imperative navigation API that’s simple to use but limited for complex navigation patterns. Best suited for straightforward apps with basic navigation needs.
Navigator 2.0: Declarative API offering more control and better web support, but with a steeper learning curve. Provides comprehensive routing capabilities for complex applications.
GoRouter: Third-party package that simplifies Navigator 2.0 while maintaining its power. Offers the best balance between simplicity and functionality for most applications.
Auto Route: Code generation package that reduces boilerplate for complex navigation scenarios. Ideal for large applications with many routes and complex navigation patterns.
Navigator 1.0 vs Navigator 2.0
Section titled “Navigator 1.0 vs Navigator 2.0”Navigator 1.0 (Imperative)
Section titled “Navigator 1.0 (Imperative)”Navigator 1.0 provides a straightforward approach to navigation:
- Push/pop-based navigation model: Simple stack-based navigation that’s intuitive to understand
- Simple and intuitive for basic navigation: Minimal learning curve for developers new to Flutter
- Less boilerplate code for simple apps: Quick implementation for straightforward navigation flows
- Limited support for deep linking: Challenges when implementing complex URL-based navigation
// Direct navigationNavigator.of(context).push(MaterialPageRoute(builder: (context) => DetailsScreen(item: item)));
// Named route navigationNavigator.of(context).pushNamed('/details', arguments: item);
Navigator 2.0 (Declarative)
Section titled “Navigator 2.0 (Declarative)”Navigator 2.0 offers more sophisticated navigation capabilities:
- Pages-based declarative approach: Navigation state is represented as a list of pages
- Better deep linking and web URL support: Comprehensive support for URL-based navigation
- More complex to implement but more powerful: Higher learning curve but greater flexibility
- Suitable for advanced navigation patterns: Handles complex scenarios like nested navigation and custom transitions
class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) { return MaterialApp.router( routerDelegate: MyRouterDelegate(), routeInformationParser: MyRouteInformationParser(), );}}
Named Routes vs Direct Navigation
Section titled “Named Routes vs Direct Navigation”Named Routes Approach
Section titled “Named Routes Approach”// Define routesMaterialApp(routes: { '/': (context) => HomeScreen(), '/details': (context) => DetailsScreen(), '/settings': (context) => SettingsScreen(),},)
// Navigate to a named routeNavigator.of(context).pushNamed('/details');
When to Use Named Routes:
- For consistent navigation paths across the app
- When you need a centralized routing configuration
- For simple navigation between fixed screens
- When building apps with predictable navigation patterns
Direct Navigation Approach
Section titled “Direct Navigation Approach”// Direct navigation with MaterialPageRouteNavigator.of(context).push(MaterialPageRoute( builder: (context) => DetailsScreen(item: selectedItem),),);
When to Use Direct Navigation:
- When passing complex data between screens
- For dynamic destination screens
- For custom transition animations
- For contextual navigation within a feature
Recommended Approach: GoRouter
Section titled “Recommended Approach: GoRouter”For most Flutter applications, we recommend using GoRouter, which offers a balance between simplicity and power:
// Basic GoRouter setupfinal _router = GoRouter(routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), ), GoRoute( path: '/details/:id', builder: (context, state) { final id = state.params['id']!; return DetailsScreen(id: id); }, ), GoRoute( path: '/settings', builder: (context, state) => SettingsScreen(), ),],);
// In MaterialAppMaterialApp.router(routerConfig: _router,);
State Management During Navigation
Section titled “State Management During Navigation”Managing state during navigation can be challenging. Here are strategies for handling state across navigation boundaries:
-
Passing Parameters For simple data, pass parameters directly through the constructor or route arguments.
-
Using BLoC/Provider For complex state, use state management solutions that persist across navigation.
-
URL Parameters For deep linking scenarios, encode state in URL parameters and parse them when navigating.
// Passing data via constructorNavigator.of(context).push(MaterialPageRoute( builder: (context) => ProductDetailsScreen( product: selectedProduct, ),),);
// In the destination screenclass ProductDetailsScreen extends StatelessWidget {final Product product;
const ProductDetailsScreen({required this.product});
@overrideWidget build(BuildContext context) { // Use product data here}}
// Access BLoC across navigation boundariesvoid navigateToDetails() {// BLoC is provided at a higher level in the widget treefinal productBloc = context.read<ProductBloc>();productBloc.add(LoadProductDetails(id: selectedId));
Navigator.of(context).pushNamed('/product-details');}
// In details screenclass ProductDetailsScreen extends StatelessWidget {@overrideWidget build(BuildContext context) { return BlocBuilder<ProductBloc, ProductState>( builder: (context, state) { if (state is ProductDetailsLoaded) { return ProductDetails(product: state.product); } return LoadingIndicator(); }, );}}
// GoRouter with parametersGoRoute(path: '/product/:id',builder: (context, state) { // Extract parameters final productId = state.params['id']!;
// Optional query parameters final showReviews = state.queryParams['reviews'] == 'true';
// Access BLoC or Provider context.read<ProductBloc>().add(LoadProductDetails(id: productId));
return ProductDetailsScreen(showReviews: showReviews);},),
// Navigate with parameterscontext.go('/product/123?reviews=true');
Deep Linking and When to Use It
Section titled “Deep Linking and When to Use It”Deep linking allows users to navigate directly to specific content within your app from external sources.
Key Components
Section titled “Key Components”URI Schemes: Custom schemes like myapp://
for app-specific links that provide direct access to your application from other apps or web browsers.
Universal Links (iOS)/App Links (Android): Regular web URLs that open your app when installed, or fall back to the web version when the app isn’t available.
Route Mapping: Translating external URLs to internal navigation paths, ensuring seamless integration between external links and your app’s navigation structure.
When to Implement Deep Linking
Section titled “When to Implement Deep Linking”Content Sharing: When users need to share specific content from your app with others, allowing direct navigation to shared items.
Marketing Campaigns: For directing users to specific features or promotions directly from email campaigns, social media, or advertisements.
Notifications: To route users to the right screen from push notifications, providing contextual navigation based on notification content.
Cross-App Integration: When other apps need to link to your app content, enabling ecosystem integration and improved user experience.
// Deep link configuration with GoRouterfinal _router = GoRouter(initialLocation: '/',routes: [ // Routes configuration],redirect: (context, state) { // Global redirects for auth, etc. return null;},);
// In Android Manifest<activity android:name=".MainActivity" android:launchMode="singleTop"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="myapp" android:host="products" /> </intent-filter></activity>
// In iOS Info.plist<key>CFBundleURLTypes</key><array><dict> <key>CFBundleURLName</key> <string>com.mycompany.myapp</string> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array></dict></array>
Navigation Architecture
Section titled “Navigation Architecture”For larger applications, we recommend structuring navigation in a feature-based manner:
Directorylib
Directorycore
Directorynavigation
- app_router.dart # Main router configuration
- route_constants.dart # Route name constants
Directoryfeatures
Directoryfeature_1
Directorynavigation
- routes.dart # Feature-specific routes
Directorypresentation
- screens
Directoryfeature_2
Directorynavigation
- routes.dart
Directorypresentation
- screens
// lib/core/navigation/app_router.dartclass AppRouter {static final GoRouter router = GoRouter( routes: [ // Home routes ...HomeRoutes.routes,
// Auth routes ...AuthRoutes.routes,
// Product routes ...ProductRoutes.routes, ], errorBuilder: (context, state) => NotFoundScreen(),);}
// lib/features/products/navigation/routes.dartclass ProductRoutes {static const String productList = '/products';static const String productDetails = '/products/:id';
static List<GoRoute> get routes => [ GoRoute( path: productList, builder: (context, state) => ProductListScreen(), ), GoRoute( path: productDetails, builder: (context, state) { final id = state.params['id']!; return ProductDetailsScreen(id: id); }, ),];}
Best Practices
Section titled “Best Practices”Navigation Best Practices
Section titled “Navigation Best Practices”Define route constants to avoid hard-coded strings throughout your application. This approach prevents typos and makes refactoring easier when routes need to change.
Pass minimum required data between screens to keep navigation lightweight and maintainable. Avoid passing entire object hierarchies when only specific properties are needed.
Separate navigation logic from UI components to maintain clean architecture and make navigation flows easier to test and modify independently.
Handle navigation errors gracefully with error screens and fallback routes. This ensures users always have a way to recover from unexpected navigation states.
Test navigation flows thoroughly, including deep links and edge cases. Navigation bugs can be particularly frustrating for users and difficult to debug in production.
Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”Navigation with State Management
Section titled “Navigation with State Management”Integrating navigation with state management is crucial for a seamless user experience:
// Navigation controlled by BLoCclass NavigationBloc extends Bloc<NavigationEvent, NavigationState> {NavigationBloc() : super(NavigationInitial()) { on<NavigateToDetails>((event, emit) { emit(NavigationDetails(id: event.id)); }); on<NavigateToHome>((event, emit) { emit(NavigationHome()); });}}
// In your widgetBlocListener<NavigationBloc, NavigationState>(listener: (context, state) { if (state is NavigationDetails) { context.go('/details/${state.id}'); } else if (state is NavigationHome) { context.go('/'); }},child: YourWidget(),)
// Navigation service with Providerclass NavigationService {final GoRouter router;
NavigationService(this.router);
void navigateToDetails(String id) { router.go('/details/$id');}
void navigateToHome() { router.go('/');}}
// Register in providerProvider.value(value: NavigationService(appRouter),),
// Use in widgetsfinal navService = context.read<NavigationService>();navService.navigateToDetails('123');
See Also
Section titled “See Also”- GoRouter Package – A declarative routing package for Flutter
- Navigator 2.0 Introduction – Deep dive into Flutter’s declarative routing
- State Management – How to manage state effectively in Flutter
- Deep Linking in Flutter – Official Flutter documentation on deep linking