Skip to content

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.

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 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 navigation
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => DetailsScreen(item: item))
);
// Named route navigation
Navigator.of(context).pushNamed('/details', arguments: item);
// Define routes
MaterialApp(
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailsScreen(),
'/settings': (context) => SettingsScreen(),
},
)
// Navigate to a named route
Navigator.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 with MaterialPageRoute
Navigator.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

For most Flutter applications, we recommend using GoRouter, which offers a balance between simplicity and power:

GoRouter Setup
// Basic GoRouter setup
final _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 MaterialApp
MaterialApp.router(
routerConfig: _router,
);

Managing state during navigation can be challenging. Here are strategies for handling state across navigation boundaries:

  1. Passing Parameters For simple data, pass parameters directly through the constructor or route arguments.

  2. Using BLoC/Provider For complex state, use state management solutions that persist across navigation.

  3. URL Parameters For deep linking scenarios, encode state in URL parameters and parse them when navigating.

// Passing data via constructor
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ProductDetailsScreen(
product: selectedProduct,
),
),
);
// In the destination screen
class ProductDetailsScreen extends StatelessWidget {
final Product product;
const ProductDetailsScreen({required this.product});
@override
Widget build(BuildContext context) {
// Use product data here
}
}

Deep linking allows users to navigate directly to specific content within your app from external sources.

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.

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 Linking Configuration
// Deep link configuration with GoRouter
final _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>

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
Structured Navigation Code
// lib/core/navigation/app_router.dart
class 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.dart
class 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);
},
),
];
}

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.

Integrating navigation with state management is crucial for a seamless user experience:

// Navigation controlled by BLoC
class 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 widget
BlocListener<NavigationBloc, NavigationState>(
listener: (context, state) {
if (state is NavigationDetails) {
context.go('/details/${state.id}');
} else if (state is NavigationHome) {
context.go('/');
}
},
child: YourWidget(),
)
  1. GoRouter Package – A declarative routing package for Flutter
  2. Navigator 2.0 Introduction – Deep dive into Flutter’s declarative routing
  3. State Management – How to manage state effectively in Flutter
  4. Deep Linking in Flutter – Official Flutter documentation on deep linking