Migration to Flutter Guide
Discover our battle-tested 21-step framework for a smooth and successful migration to Flutter!
Home
Glossary

Routing in Flutter

What is routing in Flutter?

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.

How does routing work?

Flutter uses a navigation stack:

  • Pushing a route adds a screen on top
  • Popping a route removes the current screen

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.

Why does routing matter in Flutter app development?

Routing decisions affect:

  • App scalability and structure
  • Back button consistency
  • Deep linking
  • Flutter Web support

Incorrect routing often leads to broken URLs, lost navigation history, or hard-to-maintain code.

Imperative vs declarative routing

Imperative routing (Navigator 1.0)

  • Simple push/pop API
  • Suitable for small mobile-only apps
  • Limited support for URLs and deep linking

Declarative routing (Navigator 2.0)

  • Navigation driven by route state (paths)
  • Required for Flutter Web
  • Better for complex flows

Modern Flutter apps usually rely on GoRouter or AutoRoute.

Example of routing with GoRouter

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.

Routing in Flutter Web

Flutter Web depends heavily on proper routing:

  • URLs must reflect app state
  • Browser back/forward must work
  • Page refresh should restore the correct screen

Imperative-only navigation often breaks these expectations.

Best practices for routing in Flutter

When implementing routing in your Flutter app, keep the following best practices in mind to ensure a clean, scalable, and maintainable navigation structure:

  • Before listing rules, note that routing should be designed early.
  • Prefer declarative routing for medium and large apps
  • Centralize route definitions
  • Use path parameters instead of passing large objects
  • Store route paths in constants to avoid typos
  • Test navigation on cold app start (terminated state)

Common mistakes in routing

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:

  • Treating routing as simple screen switching
  • Mixing Navigator and GoRouter randomly
  • Using Navigator.pushNamed from old tutorials (hard to maintain, poor parameter support)
  • Using a BuildContext that has no Navigator above it
  • Hardcoding route strings throughout the app

When should you use simple routing?

Use basic Navigator.push / pop or Navigator.pushNamed routing when:

  • The app is small, mobile-focused, or has only a few screens
  • Navigation flows are linear or straightforward, with minimal branching
  • Deep linking, URLs, or web support are not required
  • Quick prototypes, MVPs, or internal tools where complex navigation isn’t critical

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.

When should you avoid it?

Avoid relying on basic Navigator.push (or pushNamed) when:

  • The app targets Flutter Web or needs shareable URLs, since push/pop does not integrate with browser history.
  • Deep links or deferred links must navigate users directly to specific screens from emails, notifications, or external sources.
  • You have nested or multi-layer navigation, such as bottom tabs, drawer menus, or shell-based layouts, where navigation state must persist across multiple branches.
  • You need centralized route management or programmatic route guards for authentication, role-based access, or conditional redirects.

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.

Learn more

Flutter architecture by LeanCode

Feature-Based Flutter App Architecture - LeanCode

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.

4-Level Flutter Refactoring Framework by LeanCode

Taming Legacy Code in Flutter: Our Refactoring Framework for Enterprise Flutter Projects

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.