Flutter - Navigation Between Screens with Named Routes

In Flutter, we can use Navigation.push to navigate between screens. That function has two parameters, the first is Context and the second is WidgetBuilder. The problem of using Navigation.push is if there's a screen that can be accessed from multiple screens, it will lead to code duplication because we have to define the same WidgetBuilder function multiple times. An easy and clean solution to overcome that problem is by using named routes - just define a list of route names along with WidgetBuilder function for each, then use the name of the route to execute the function

First, create three simple screens: HomeScreen, SecondScreen and ThirdScreen. For this tutorial, we assume that SecondScreen and ThirdScreen are on the same level, which means if you navigate from HomeScreen to SecondScreen, then to ThirdScreen, the previous screen on SecondScreen and ThirdScreen is the same: HomeScreen. The HomeScreen contains a button for navigating to SecondScreen. The SecondScreen has two buttons, the upper is for navigating to the ThirdScreen, while the second buton is for returning to the previous screen. The ThirdScreen has two buttons, the upper is for navigating to the SecondScreen, while the second button is for returning to the previous screen. We will add the code inside onPressed later.

Create the Screens

  class HomeScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Home Screen'),
        ),
        body: Center(
          child: RaisedButton(
            child: Text('Go to second screen'),
            onPressed: () {

            },
          ),
        ),
      );
    }
  }
  
  class SecondScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text("Second Screen"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              RaisedButton(
                child: Text('Go to third screen'),
                onPressed: () {

                },
              ),
              RaisedButton(
                child: Text('Pop!'),
                onPressed: () {

                },
              ),
            ],
          )
        ),
      );
    }
  }
  
  class ThirdScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text("Third Screen"),
        ),
        body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                  child: Text('Go to second screen'),
                  onPressed: () {

                  },
                ),
                RaisedButton(
                  child: Text('Pop!'),
                  onPressed: () {

                  },
                ),
              ],
            )
        ),
      );
    }
  }

Define Routes

To use named routes, we need to define the list of routes. Defining routes using MaterialApp is quite easy. On the constructor, pass routes option, which is an object whose key is the route name and the value is a WidgetBuilder function that will be executed if the route name matches. In this case, the function calls the screen that will be launched, as examplified below. You can define as many routes as you want, but there is one required route you have to define: /. For specifying the first active route when the application launches, use initialState.

  class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        initialRoute: '/',
        routes: {
          '/': (context) => HomeScreen(),
          '/second': (context) => SecondScreen(),
          '/third': (context) => ThirdScreen(),
        },
      );
    }
  }

Push

The navigation history is saved in a stack. If you want to add the new screen to the stack while navigating, use Navigator.pushNamed function, with context as the first argument and the route name as the second argument.

  Navigator.pushNamed(context, '/second');

Pop

To pop a screen from history stack, use Navigator.pop with context as the first argument.

  Navigator.pop(context);

Pop and Push Named

If we want to make SecondScreen and ThirdScreen as screens with the same level, the expected behavior is SecondScreen popped from the stack and the ThirdScreen pushed to the stack. It can be done using Navigation.popAndPushNamed with context as the first argument. The animations for the pop and the push are performed simultaneously, so the route below (in this case HomeScreen may be briefly visible even if both the old route and the new route are opaque.

  Navigator.popAndPushNamed(context);

Push Replacement Named

pushReplacementNamed is very similar to popAndPushNamed. The difference is the removed route's exit animation is not run.

  Navigator.pushReplacementNamed(context);

Below is the full code.

  import 'package:flutter/material.dart';
  
  class HomeScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Home Screen'),
        ),
        body: Center(
          child: RaisedButton(
            child: Text('Go to second screen'),
            onPressed: () {
              Navigator.pushNamed(context, '/second');
            },
          ),
        ),
      );
    }
  }
  
  class SecondScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text("Second Screen"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              RaisedButton(
                child: Text('Go to third screen'),
                onPressed: () {
                  Navigator.popAndPushNamed(context, '/third');
                },
              ),
              RaisedButton(
                child: Text('Pop!'),
                onPressed: () {
                  Navigator.pop(context);
                },
              ),
            ],
          )
        ),
      );
    }
  }
  
  class ThirdScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text("Third Screen"),
        ),
        body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                  child: Text('Go to second screen'),
                  onPressed: () {
                    Navigator.pushReplacementNamed(context, '/second');
                  },
                ),
                RaisedButton(
                  child: Text('Pop!'),
                  onPressed: () {
                    Navigator.pop(context);
                  },
                ),
              ],
            )
        ),
      );
    }
  }
  
  class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        initialRoute: '/',
        routes: {
          '/': (context) => HomeScreen(),
          '/second': (context) => SecondScreen(),
          '/third': (context) => ThirdScreen(),
        },
      );
    }
  }
  
  Future<void> main() async {
    runApp(MyApp());
  }