Implementing Navigation and Routing in Flutter
Contents
-
Core Concepts
-
Implementing Imperative Navigation
-
Implementing Declarative Navigation
-
Implementing Nested Navigation
-
Workflows
-
Examples
Core Concepts
-
Routes: In Flutter, screens and pages are referred to as routes. A route is simply a widget. This is equivalent to an Activity in Android or a ViewController in iOS.
-
Navigator vs. Router:
-
Use Navigator (Imperative) for small applications without complex deep linking requirements. It manages a stack of Route objects.
-
Use Router (Declarative) for applications with advanced navigation, web URL synchronization, and specific deep linking requirements.
-
Deep Linking: Allows an app to open directly to a specific location based on a URL. Supported on iOS, Android, and Web. Web requires no additional setup.
-
Named Routes: Avoid using named routes (MaterialApp.routes and Navigator.pushNamed ) for most applications. They have rigid deep linking behavior and do not support the browser forward button. Use a routing package like go_router instead.
Implementing Imperative Navigation
Use the Navigator widget to push and pop routes using platform-specific transition animations (MaterialPageRoute or CupertinoPageRoute ).
Pushing and Popping
-
Navigate to a new route using Navigator.push(context, route) .
-
Return to the previous route using Navigator.pop(context) .
-
Use Navigator.pushReplacement() to replace the current route, or Navigator.pushAndRemoveUntil() to clear the stack based on a condition.
Passing and Returning Data
-
Sending Data: Pass data directly into the constructor of the destination widget. Alternatively, pass data via the settings: RouteSettings(arguments: data) parameter of the PageRoute and extract it using ModalRoute.of(context)!.settings.arguments .
-
Returning Data: Pass the return value to the pop method: Navigator.pop(context, resultData) . Await the result on the pushing side: final result = await Navigator.push(...) .
Implementing Declarative Navigation
For apps requiring deep linking, web URL support, or complex routing, implement the Router API via a declarative routing package like go_router .
-
Switch from MaterialApp to MaterialApp.router .
-
Define a router configuration that parses route paths and configures the Navigator automatically.
-
Navigate using package-specific APIs (e.g., context.go('/path') ).
-
Page-backed vs. Pageless Routes: Declarative routes are page-backed (deep-linkable). Imperative pushes (e.g., dialogs, bottom sheets) are pageless. Removing a page-backed route automatically removes all subsequent pageless routes.
Implementing Nested Navigation
Implement nested navigation to manage a sub-flow of screens (e.g., a multi-step setup process or persistent bottom navigation tabs) independently from the top-level global navigator.
-
Instantiate a new Navigator widget inside the host widget.
-
Assign a GlobalKey<NavigatorState> to the nested Navigator to control it programmatically.
-
Implement the onGenerateRoute callback within the nested Navigator to resolve sub-routes.
-
Intercept hardware back button presses using PopScope to prevent the top-level navigator from popping the entire nested flow prematurely.
Workflows
Workflow: Standard Screen Transition
Copy this checklist to track progress when implementing a basic screen transition:
-
Create the destination widget (Route).
-
Define required data parameters in the destination widget's constructor.
-
Implement Navigator.push() in the source widget.
-
Wrap the destination widget in a MaterialPageRoute or CupertinoPageRoute .
-
Implement Navigator.pop() in the destination widget to return.
Workflow: Implementing Deep-Linkable Routing
Use this conditional workflow when setting up app-wide routing:
-
If the app is simple and requires no deep linking:
-
Use standard MaterialApp and Navigator.push() .
-
If the app requires deep linking, web support, or complex flows:
-
Add the go_router package.
-
Change MaterialApp to MaterialApp.router .
-
Define the GoRouter configuration with all top-level routes.
-
Replace Navigator.push() with context.go() or context.push() .
Workflow: Creating a Nested Navigation Flow
Run this workflow when building a multi-step sub-flow (e.g., IoT device setup):
-
Define string constants for the nested route paths.
-
Create a GlobalKey<NavigatorState> in the host widget's state.
-
Return a Navigator widget in the host's build method, passing the key.
-
Implement onGenerateRoute in the nested Navigator to map string paths to specific step widgets.
-
Wrap the host Scaffold in a PopScope to handle back-button interceptions (e.g., prompting "Are you sure you want to exit setup?").
-
Use navigatorKey.currentState!.pushNamed() to advance steps within the flow.
Examples
Example: Passing Data via Constructor (Imperative)
// 1. Define the data model class Todo { final String title; final String description; const Todo(this.title, this.description); }
// 2. Source Screen class TodosScreen extends StatelessWidget { final List<Todo> todos; const TodosScreen({super.key, required this.todos});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Todos')), body: ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { return ListTile( title: Text(todos[index].title), onTap: () { // Push and pass data via constructor Navigator.push( context, MaterialPageRoute( builder: (context) => DetailScreen(todo: todos[index]), ), ); }, ); }, ), ); } }
// 3. Destination Screen class DetailScreen extends StatelessWidget { final Todo todo; const DetailScreen({super.key, required this.todo});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(todo.title)), body: Padding( padding: const EdgeInsets.all(16), child: Text(todo.description), ), ); } }
Example: Nested Navigation Flow
class SetupFlow extends StatefulWidget { final String initialRoute; const SetupFlow({super.key, required this.initialRoute});
@override State<SetupFlow> createState() => _SetupFlowState(); }
class _SetupFlowState extends State<SetupFlow> { final _navigatorKey = GlobalKey<NavigatorState>();
void _exitSetup() => Navigator.of(context).pop();
@override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) async { if (didPop) return; // Intercept back button to prevent accidental exit _exitSetup(); }, child: Scaffold( appBar: AppBar(title: const Text('Setup')), body: Navigator( key: _navigatorKey, initialRoute: widget.initialRoute, onGenerateRoute: _onGenerateRoute, ), ), ); }
Route<Widget> _onGenerateRoute(RouteSettings settings) { Widget page; switch (settings.name) { case 'step1': page = StepOnePage( onComplete: () => _navigatorKey.currentState!.pushNamed('step2'), ); break; case 'step2': page = StepTwoPage(onComplete: _exitSetup); break; default: throw StateError('Unexpected route name: ${settings.name}!'); }
return MaterialPageRoute(
builder: (context) => page,
settings: settings,
);
} }