Routing in Flutter is the mechanism that controls navigation between screens in an application. Each screen is represented as a route, and routing defines how users move forward, backward, or directly to a specific screen (via URLs or deep links).
Routing is not only about UI flow, it also affects browser URLs, back button behavior, and app state restoration.
Flutter uses a navigation stack:
Example of a classic imperative:
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => SelectPage()),
);The important detail: Navigator.push returns a Future.
You can return data from the next screen:
Navigator.pop(context, 'Selected value');This pattern is commonly used for pickers, forms, and selection dialogs.
Routing decisions affect:
Incorrect routing often leads to broken URLs, lost navigation history, or hard-to-maintain code.
Imperative routing (Navigator 1.0)
Declarative routing (Navigator 2.0)
Modern Flutter apps usually rely on GoRouter or AutoRoute.
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (_, __) => HomePage(),
),
GoRoute(
path: '/details/:id',
builder: (_, state) =>
DetailsPage(id: state.pathParameters['id']!),
),
],
);This enables URL-based navigation like /details/42.
Flutter Web depends heavily on proper routing:
Imperative-only navigation often breaks these expectations.
When implementing routing in your Flutter app, keep the following best practices in mind to ensure a clean, scalable, and maintainable navigation structure:
Even experienced Flutter developers can run into routing issues, so being aware of these common mistakes can help you avoid fragile or hard-to-maintain navigation setups:
Navigator and GoRouter randomlyNavigator.pushNamed from old tutorials (hard to maintain, poor parameter support)BuildContext that has no Navigator above itUse basic Navigator.push / pop or Navigator.pushNamed routing when:
Named routes (pushNamed) allow some centralization of screen names, making transitions easier to manage without introducing a full routing framework. However, they share the same limitations as basic push/pop when targeting Flutter Web or handling deep links.
In these cases, simple routing keeps the code lightweight and easy to understand, without adding extra dependencies or boilerplate. It works well when each screen transition can be handled locally, without needing a centralized navigation architecture.
Avoid relying on basic Navigator.push (or pushNamed) when:
In such scenarios, using a routing package like GoRouter or auto_route provides predictable navigation behavior, better state management, and easier handling of complex app flows, including route observers, URL synchronization, and conditional redirects.
12 min • Jul 27, 2023
Read about the LeanCode approach to Flutter architecture. We highlight some of the design decisions that should be made when developing a feature in mobile apps. This includes our approach to dependency injection, state management, widget lifecycle, and data fetching.
11 min • Nov 25, 2025
Legacy code in Flutter? It happens, and it’s a sign of success. Fast growth creates technical debt. At LeanCode, we’ve helped enterprises untangle it with a proven framework that restores clarity, scalability, and speed without a costly rewrite.