Navigation and routing in Flutter

Navigation is one of the topics that causes the most headaches when you start with Flutter... and, curiously, also one of the things that makes your life easiest when you manage to master it. In my first projects, I realized that almost everything relies on the same thing: managing a route stack as if it were a real stack (push, pop... and a little more magic).

In this guide, I take you from scratch to advanced: basic Navigator, Navigator 2.0, go_router, nested routes, practical case studies, and real-world best practices that have saved my projects more than once.

We can compare navigation in Flutter with the way the Stack data structure works. 

We continue from the previous post in which we learned how to use FutureBuilder, async, await in Flutter.

What is Navigation in Flutter and How the Route Stack Works

How Flutter Manages Screens as Routes

In Flutter, each screen is a route that is stacked on top of another. When you navigate, you push a route onto the stack; when you go back, you remove it.

When I understood it this way—as a simple stack—everything made sense: "If I push 5 screens, I'll need 5 pops to go back." And truly, in my first apps (product lists, detail screens, etc.), having this mental analogy helped me a lot to avoid silly mistakes.

Differences between Mobile Navigation and Flutter Web

In mobile apps:

  • animation takes precedence
  • transitions matter
  • the history is internal to the app

In Flutter Web:

  • routes become real URLs
  • the browser's “Back” button matters
  • you need your navigation to be declarative

Navigator in Flutter: Essential Concepts and First Steps

Note that screens in Flutter are called routes, but for simplicity, I will use the word page to refer to screens. Flutter uses the Navigator class for navigation. There are five methods in the Navigator class that we will discuss in this article:

  1. Navigator.push()
  2. Navigator.pop()
  3. Navigator.pushReplacement()
  4. Navigator.pushAndRemoveUntil()
  5. Navigator.popUntil()

When we are creating an application like an online shopping app, we want to show the details of an item on a new page when the user selects an item from a list and we also want to return to the home page when the user clicks back. This can be implemented using the methods:

  1. Navigator.push()
  2. Navigator.pop()

The Navigator class provides all navigation capabilities in a Flutter application.

Navigator provides methods to mutate the stack by adding and removing pages from it using the methods noted above. The Navigator.push method is for navigating to a newer page, and Navigator.pop is for going back from the current page.

These are basic examples of pop and push: the push method takes BuildContext as the first argument, and the second argument is PageBuilder. 

Navigator.push and Navigator.pop Explained Practically

  • Navigator.push() adds a new screen.
    Navigator.pop() takes you back.

When I built my first app with detail screens, I experienced it like this: "Press an item → push; go back → pop.” Simple, direct, effective.

Navigator.push( context, MaterialPageRoute(builder: (_) => DetalleScreen()), );

pushReplacement, pushAndRemoveUntil, and popUntil in Real Flows

The most important methods:

  • pushReplacement(): ideal for login → home screens.
    In one of my projects, I used pushReplacement to prevent the user from going back to the login screen... and it was a godsend.
  • pushAndRemoveUntil(): perfect for logging out or resetting a flow.
    In an app with a checkout, when the payment was completed, I cleared the entire stack before taking the user to the summary.
  • popUntil(): useful when you want to return to a specific screen.
    I used it to jump from the payment screen directly to the confirmation screen without the user seeing the intermediate screens backward.

Passing data between screens: arguments, ModalRoute, and results

You can pass arguments like this:

Navigator.pushNamed( context, '/detalle', arguments: {'id': 25, 'nombre': 'Pepe'}, );

And you retrieve them with:

final args = ModalRoute.of(context)!.settings.arguments;

Named Routes: When They Are Convenient and How to Organize Them

Named routes allow you to change the route by using strings instead of providing component classes, which in turn allows you to reuse code.

  • repetitive navigation
  • greater maintainability
  • structured routes
  • filters, parameters, details, etc.

Route Definition

The route is a map with string keys and values like constructors that are passed to the routes property in MaterialApp:

void main() { runApp(MaterialApp( title: 'My App', home: Main(), // Routes defined here routes: { "page":(context)=>PageRoute() }, )); }

Using Named Routes

Instead of push, pushNamed is used to change to a new route. Similarly, *pushReplacementNamed* is used instead of pushReplacement. The pop method is the same for all routes.

class Main extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Route'),
      ),
      body: Center(
        child:RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.pushReplacementNamed(context, "page");
          },
        ),
      ),
    );
  }

Navigator 2.0: Declarative Navigation and History Management

What Navigator 2.0 Solves and Why It's Key in Flutter Web

Navigator 2.0 was created to solve:

  • complex deep linking
  • syncing UI ↔ URL
  • non-linear flows
  • greater history control

In a web app I built, using RouterDelegate allowed the URL to always reflect the actual navigation state.

RouterDelegate and RouteInformationParser in Simple Terms

  • RouteInformationParser: interprets the URL.
  • RouterDelegate: decides which screens to show.

Practical Case: URL-Based Navigation and Deep Linking

Deep linking lets someone open your app directly to:

  • /product/123
  • /profile/edit
  • /task/7

Navigator 2.0 supports this natively.

go_router in Flutter: Modern and Declarative Navigation
Why go_router Simplifies Navigation in Large Apps

Here comes the part that changed my life as a Flutter developer: go_router.

In my large projects, organized by features, I noticed that Navigator 1.0 became unmanageable. go_router gave me:

  • declarative routes
  • clean URLs
  • clearer parameters
  • easy nested routes
  • powerful redirects

Declarative Definition of Routes and Dynamic Parameters

GoRoute(
 path: 'details/:id',
 builder: (_, state) => DetalleScreen(id: state.params['id']!),
);

Using context.go, goNamed, and Parameter Extraction

  • context.go('/detalles')
  • context.goNamed('detalle', params: {'id': '8'})

Redirects, Route Protection, and State Control

You can block unauthorized routes:

redirect: (_, state) {
 if (!logueado) return '/login';
 return null;
}

Custom Transitions with go_router

You can animate each route with CustomTransitionPage and transitionsBuilder.

Nested Routes and Project Organization by Features

How to Structure Modules and Child Routes with go_router

Here is your perfect example: task list ⇒ details ⇒ add task.

go_router allows declaring child routes:

GoRoute(
 path: '/',
 routes: [
   GoRoute(
     path: 'details/:taskId',
     builder: ...
   ),
 ],
);

Practical Case: Task List with Details and Creation Screen

When I implemented a task app, I discovered something key:

  • task list is the parent route
  • task details is the child route
  • add task is another child
  • everything remains legible and modular

Benefits of Nested Navigation in Scalable Apps

  • Easier maintenance
  • Predictable URLs
  • Separation by features
  • Full flow control

Full Comparison: Navigator vs Navigator 2.0 vs go_router

  • What to choose according to application size and complexity
  • Scenario    Best Option
  • Small app    Navigator 1.0
  • Medium app with simple states    Named Routes
  • Large/modular app    go_router
  • Flutter Web app    Navigator 2.0 or go_router
  • Advanced deep linking    go_router or Nav 2.0

Comparative Table of Real-World Use Cases

  • Real Case    Recommended Solution
  • Avoiding returning to login    pushReplacement / redirect
  • URL-based navigation    go_router / Nav 2.0
  • Checkout-style flows    pushAndRemoveUntil
  • Child routes    go_router
  • Web-style SPA    go_router

Best Practices to Avoid Common Mistakes

  • Don't mix Navigator 1.0 and go_router unnecessarily
  • Keep routes outside the UI
  • Use route names, not hardcoded paths
  • Avoid state coupled to routes

Best Practices and Patterns for Robust Navigation

Organizing Routes by Modules or Features

In medium or large apps, I always use folders by feature:

/features/tasks
 /presentation
 /routes
 /data

State Management in Navigation Flows

Depending on the size:

  • small → setState
  • medium → provider
  • large → riverpod or bloc

Code Cleanup and Recommended Architecture

  • Separate navigation from the UI
  • Use models for complex parameters
  • Avoid logic in route builders

DO NOT use push(), use pushReplacement() in the Navigator with the routes

Video thumbnail

I wanted to share an anecdote that happened to me when creating the DesarrolloLibre application, which is the one you are seeing on screen.

The problem arose with the DrawerComponent, which is what allows me to navigate between the various pages of the application. Specifically, the inconvenience occurred in the listing of all courses: when I entered the detail of one course and then wanted to go to the detail of another, I had to click on the DrawerComponent and then on "All courses."

Using Navigator.push()

Previously, I was using:

Navigator.push();

The problem with Navigator.push() is that it enqueues each of the previous views. Therefore, if you navigated between several courses, pressing "Back" returned you to the previous course. For example, if you needed to check 10 courses, 10 previous views had been enqueued.

This generated an additional problem: when dispose() was executed in the courses widget, the state of the current page was mixed with that of the previous pages. This caused the application to crash by going out of range when trying to consume one of the class listings.

Solution with Navigator.pushReplacement()

The important thing is to identify what your application truly needs to function correctly.

In my case, it made no sense to keep all the previous views enqueued, as they only consumed unnecessary resources.

The best solution was to use:

Navigator.pushReplacement();

With this, every time we navigate to a new course, the previous page is replaced, avoiding the accumulation of views and the state problems I mentioned.

Frequently Asked Questions About Navigation in Flutter

  • Differences between go and goNamed
    • go → uses the direct path
    • goNamed → uses the name (safer and more scalable)
  • How to protect routes with redirects
    • With redirect in go_router or by using guards in RouterDelegate.
  • How to pass additional data to a route
    • With extra:
    • context.goNamed('detalle', extra: {'modo': 'edicion'});
  • How to control history in Flutter Web
    • Use declarative navigation and avoid manually manipulating the stack.

Conclusion: How to Choose the Best Navigation Strategy for Your Flutter App

Navigation is more than just going from one screen to another: it's the backbone of your app's flow. With Navigator 1.0, you can move fast; with Navigator 2.0, you gain control; and with go_router, you achieve modern, declarative, and scalable navigation.

In my experience building apps with lists, details, login, payment flows, and web versions, what has worked best for me is:

  • Navigator 1.0 for prototypes and simple apps
  • go_router for anything moderately serious
  • Navigator 2.0 if I need surgical control of history or deep linking

Mastering all three tools will make you faster, more flexible, and allow you to create robust apps without getting tangled up in navigation.

Next step, discover Flutter's Scaffold Widget.

I agree to receive announcements of interest about this Blog.

Let's see how to work with navigation in Flutter using the following functions: Navigator.push() Navigator.pop() Navigator.pushReplacement() Navigator.pushAndRemoveUntil() Navigator.popUntil() We'll also see how to use Go Router for modern browsing in Flutter.

| 👤 Andrés Cruz

🇪🇸 En español