Navigation is a core part of any mobile, web, or desktop application. A user-friendly navigation structure ensures your app feels intuitive, while a well-organized routing system keeps your code clean and maintainable.
Flutter offers multiple ways to handle navigation—from simple stack-based navigation to advanced declarative routing with packages like go_router. In this blog, we’ll cover best practices for clean and scalable navigation in Flutter, with practical examples.
Navigator
The simplest form of navigation is stack-based using Navigator.push() and Navigator.pop().
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailsPage()),
);
Navigator.pop(context);
- Navigator.push
- Navigator is a built-in Flutter class that manages a stack of screens (routes).
- push adds a new screen on top of the current one, so the user can go back later using Navigator.pop() or the back button.
- Context
- Every widget in Flutter has a BuildContext.
- It’s required here so the Navigator knows which part of the widget tree it should operate on.
- MaterialPageRoute
- This is a route that uses a Material design animation when transitioning between screens.
- You can also use CupertinoPageRoute for iOS-style transitions.
- builder: (context) => DetailsPage()
- builder is a function that returns the widget you want to navigate to.
- Here, it creates an instance of DetailsPage.
While this navigation approach is simple and works well for small apps, it can become difficult to manage in larger apps with complex flows or deep linking.
Named Routes
Named routes centralize your routes in MaterialApp:
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/details': (context) => DetailsPage(),
},
);
This defines named routes in a Flutter app, allowing navigation between screens using route names instead of directly pushing widgets.
- MaterialApp
- This is the root widget for a Material Design app in Flutter.
- It provides theme, navigation, localization, and other app-level configurations.
- initialRoute: '/'
- Specifies the first screen that opens when the app launches.
- '/' is the default home route.
- routes
- A map of route names to widget builders.
- Each key is a string route name, and the value is a function that returns the corresponding screen widget.
- In this example:
- '/details' > DetailsPage()
Navigate with:
Navigator.pushNamed(context, '/details');
The system looks up the route name in the routes map and displays the corresponding page.
You can also pass arguments like below:
Navigator.pushNamed(
context,
'/details',
arguments: 'Hello Details Page',
);
Using named routes is cleaner than inline routing and makes managing multiple screens easier, but it offers limited flexibility for dynamic arguments or complex deep links.
onGenerateRoute (Dynamic Routing)
For apps that require dynamic routes or argument passing:
MaterialApp(
onGenerateRoute: (settings) {
switch (settings.name) {
case '/details':
final id = settings.arguments as int;
return MaterialPageRoute(builder: (_) => DetailsPage(id: id));
default:
return MaterialPageRoute(builder: (_) => NotFoundPage());
}
},
);
This uses onGenerateRoute to handle navigation dynamically, allowing you to pass arguments and manage routes that aren’t predefined in a static map.
- MaterialApp
- Root widget for a Flutter app with Material Design.
- onGenerateRoute allows you to generate routes dynamically instead of using a fixed routes map.
- onGenerateRoute: (settings) { ... }
- Called whenever Navigator.pushNamed is used.
- Receives a RouteSettings object containing:
- settings.name - the route name being navigated to.
- settings.arguments - optional arguments passed to the route.
- switch (settings.name)
- Checks the route name to decide which page to show.
- Example:
- If settings.name == '/details' - navigate to DetailsPage and pass an id argument.
- Otherwise > navigate to NotFoundPage().
Using onGenerateRoute is flexible and allows dynamic arguments, but it adds more boilerplate and still follows an imperative navigation style.
go_router (Recommended for Large Apps)
go_router is officially recommended by Flutter for declarative, web-friendly routing.
final GoRouter router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/details/:id',
builder: (context, state) {
final id = state.params['id']!;
return DetailsPage(id: id);
},
),
],
);
This defines declarative routing using the go_router package in Flutter. It supports named parameters, deep linking, and clean navigation.
- GoRouter
- A modern routing solution for Flutter that handles navigation declaratively.
- It integrates deep linking, URL parameters, and navigation state seamlessly.
- routes: [ ... ]
- A list of route definitions for the app.
- GoRoute(path: '/', builder: ...)
- Defines the home route '/'.
- builder returns the widget (HomePage) when this route is visited.
- GoRoute(path: '/details/:id', builder: ...)
- Defines a route with a path parameter :id.
- Example URL: /details/42 > id = '42'.
- state.params['id'] retrieves the path parameter.
- Passing parameters
final id = state.params['id']!; - return DetailsPage(id: id);
- The extracted id is passed to DetailsPage.
GoRouter offers clean declarative syntax, handles deep links automatically, supports nested routes, and integrates easily with Flutter web and desktop apps.
To read more about What are the Types of Data Storage in Flutter, refer to our blog What are the Types of Data Storage in Flutter.