App Development
Blog

Handbook for Navigation in Flutter

by
Subir Chakraborty
May 13, 2021

One of the crucial things every developer comes across when an app has more than two screens (which is true in most cases) is a need to navigate from one screen to another. Most apps contain several screens for different types of information. Sometimes you need to navigate from A -> B and sometimes, you have additional requirements.

These may be:

  •  Navigate from A->B
  •  Navigate from A->B with some Parameters
  •  Navigate from A->B with some Arguments
  •  Navigate from A->B with some Transition Animation


Flutter offers solutions for the requirements above with:

  • A navigator widget
  • Popular 3rd party Libraries


Navigator widget


A widget that manages a set of child widgets with a stack discipline.

A MaterialPageRoute is useful because it transitions to the new route using a platform-specific animation.


Using the Navigator API

Mobile apps typically reveal their contents via full-screen elements called screens or pages. In Flutter, these elements are called Routes and they're managed by a Navigator widget. The navigator manages a stack of Route objects and provides two ways for managing the stack, the declarative API Navigator.pages or the imperative API Navigator.push and Navigator.pop.


Using the Pages API

The Navigator will convert its Navigator.pages into a stack of Routes, if it is provided. A change in Navigator.pages will trigger an update to the stack of Routes. The Navigator will update its routes to match the new configuration of its Navigator.pages. To use this API, one can use CustomBuilderPage or create a Page subclass and define a list of Pages for Navigator.pages. A Navigator.onPopPage callback is also required to properly clean up the input pages in case of a pop.


Lets See It In Action

We have few approaches for creating navigation between routes:

1. Navigate to a new screen and back

2. Navigate with named routes

3. Pass arguments to a named route

4. Return data from a screen


Navigate to a new screen and back

In Android, a route is equivalent to an Activity. In iOS, a route is equivalent to a ViewController. In Flutter, a route is just a widget.


Lets understand how it works by creating navigation between two routes, using these steps:

1. Create two routes.

2. Navigate to the second route using Navigator.push().

3. Return to the first route using Navigator.pop().


1. Create two routes

First, create two routes to work with. Since this is a basic example, each route contains only a single button. Tapping the button on the first route navigates to the second route. Tapping the button on the second route returns to the first route.

First, set up the visual structure:

class FirstRoute extends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text('First Route'),

      ),

      body: Center(

        child: RaisedButton(

          child: Text(‘Navigate to second route'),

          onPressed: () {

            // Navigate to the second route when tapped.

          },

        ),

      ),

    );

  }}

class SecondRoute extends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text("Second Route"),

      ),

      body: Center(

        child: RaisedButton(

          onPressed: () {

            // Navigate back to the first route when tapped.

          },

          child: Text('Go back!'),

        ),

      ),

    );

  }

}


2. Navigate to the second route using Navigator.push().

To switch to a new route, use the Navigator.push() method. The push() method adds a Route to the stack of routes managed by the Navigator. Where does the Route come from? 

You can create your own, or use a MaterialPageRoute, which is useful because it transitions to the new route using a platform-specific animation.

In the build() method of the FirstRoute widget, update the onPressed() callback:

// Within the `FirstRoute` widget

onPressed: () {

  Navigator.push(

    context,

    MaterialPageRoute(builder: (context) => SecondRoute()),

  );}


3. Return to the first route using Navigator.pop()

How do you close the second route and return to the first? By using the Navigator.pop() method. The pop() method removes the current Route from the stack of routes managed by the Navigator.

To implement a return to the original route, update the onPressed() callback in the SecondRoute widget:

// Within the SecondRoute widget

onPressed: () {

  Navigator.pop(context);

}


Navigate with named routes

If you need to navigate to the same screen in many parts of your app, the above approach can result in code duplication. A better solution is to define a named route, and use the named route for navigation. To work with named routes, we use the Navigator.pushNamed() function.


Define the routes by providing additional properties to the MaterialApp constructor: the initialRoute and the routes themselves.

The initialRoute property defines which route the app should start with. The routes property defines the available named routes and the widgets to build when navigating to those routes.

// Start the app with the "/home" named route. In this case, the app starts on the MyHomePage widget.

initialRoute: '/home',

routes: {

 // When navigating to the "/home" route, build the MyHomePage widget.

 '/home': (context) => MyHomePage(title: 'Navigation Demo Home Page'),


 // When navigating to the "/secondRoute'" route, build the SecondRoute widget.

'/secondRoute': (context) => SecondRoute()


Navigate to the second screen

With the widgets and routes in place, trigger navigation by using the Navigator.pushNamed() method. This tells Flutter to build the widget defined in the routes table and launch the screen.

// Navigate to the SecondRoute screen using a named route.

Navigator.pushNamed(context, '/secondRoute);

Return to the first screen

To navigate back to the first screen, the Navigator.pop() function will be used as it is in the approach above.

// Within the SecondRoute widget

onPressed: () {

  Navigator.pop(context);

}


Pass arguments to a named route

The Navigator provides the ability to navigate to a named route from any part of an app using a common identifier. In some cases, you might also need to pass arguments to a named route. For example, you might wish to navigate to the /user route and pass information about the user to that route.

You can accomplish this task using the arguments parameter of the Navigator.pushNamed() method. Extract the arguments using the ModalRoute.of() method or inside and Generate Route() function provided to the MaterialApp or CupertinoApp constructor.

Lets understand how it works, using these steps:


1. Define the arguments you need to pass.

2. Create a widget that extracts the arguments.

3. Register the widget in the routes table.

4. Navigate to the widget.


1. Define the arguments you need to pass

First, define the arguments you need to pass to the new route. In this example, pass two pieces of data: The Title of the Screen and a Message.

To pass both pieces of data, create a class that stores this information.

// You can pass any object to the arguments parameter.

class ScreenArguments {

  final String title;


  ScreenArguments(this.title);

}


2. Create a widget that extracts the arguments


Next, create a widget that extracts and displays the title and message from the ScreenArguments. To access the ScreenArguments, use the ModalRoute.of() method. This method returns the current route with the arguments.

// A widget that extracts the necessary arguments from the ModalRoute.

class ExtractArgumentsScreen extends StatelessWidget {

  static const routeName = '/extractArguments';


  @override

  Widget build(BuildContext context) {

    // Extract the arguments from the current ModalRoute settings and cast

    // them as ScreenArguments.

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


    return Scaffold(

      appBar: AppBar(

        title: Text(args.title),

      ),

      body: Center(

        child: Text(args.message),

      ),

    );

  }

}


3. Register the widget in the routes table.

Next, add an entry to the routes provided to the MaterialApp widget. The routes define which widget should be created based on the name of the route.

MaterialApp(

  routes: {

    ExtractArgumentsScreen.routeName: (context) => ExtractArgumentsScreen(),

  },

);


4. Navigate to the widget

Finally, navigate to the ExtractArgumentsScreen when a user taps a button using Navigator.pushNamed(). Provide the arguments to the route via the arguments property. The ExtractArgumentsScreen extracts the title and message from these arguments.

// A button that navigates to a named route. The named route

// extracts the arguments by itself.

RaisedButton(

  child: Text("Navigate to screen that extracts arguments"),

  onPressed: () {

    // When the user taps the button, navigate to a named route

    // and provide the arguments as an optional parameter.

    Navigator.pushNamed(

      context,

      ExtractArgumentsScreen.routeName,

      arguments: ScreenArguments(

        'Extract Arguments Screen',

        'This message is extracted in the build method.',

      ),

    );

  },

),


Alternatively, extract the arguments using onGenerateRoute [Step 2 Alternative]


Instead of extracting the arguments directly inside the widget, you can also extract the arguments inside an onGenerateRoute() function and pass them to a widget. The onGenerateRoute() function creates the correct route based on the given RouteSettings.

MaterialApp(

  // Provide a function to handle named routes. Use this function to

  // identify the named route being pushed, and create the correct

  // screen.

  onGenerateRoute: (settings) {

    // If you push the PassArguments route

    if (settings.name == PassArgumentsScreen.routeName) {

      // Cast the arguments to the correct type: ScreenArguments.

      final ScreenArguments args = settings.arguments;


      // Then, extract the required data from the arguments and

      // pass the data to the correct screen.

      return MaterialPageRoute(

        builder: (context) {

          return PassArgumentsScreen(

            title: args.title,

            message: args.message,

          );

        },

      );

    }});


Return data from a screen

In some cases, you might want to return data from a new screen. You can do this with the Navigator.pop() method.

When a route is pushed to ask the user for a value, the value can be returned via the pop method's result parameter. Methods that push a route return a Future. The Future resolves when the route is popped and the Future's value is the pop method's result parameter. For example if we wanted to ask the user to press 'OK' to confirm an operation we could await the result of Navigator.push:

// A method that launches the SelectionScreen and awaits the

// result from Navigator.pop.

  _navigateAndDisplaySelection(BuildContext context) async {

    // Navigator.push returns a Future that completes after calling

    // Navigator.pop on the Selection Screen.

    final result = await Navigator.push(

      context,

      MaterialPageRoute(builder: (context) => SelectionScreen()),

    );


// After the Selection Screen returns a result

// show the new result.

    Scaffold.of(context).showSnackBar(SnackBar(content: Text("$result")));

  }

  }


  //Navigator.pop form SelectionScreen.

RaisedButton(

  onPressed: () {

    // The Yep button returns "Yep!" as the result.

    Navigator.pop(context, 'Yep!');

  },

  child: Text('Yep!'),

);


Additional Approaches for Navigation

1. Popup routes

2. Custom routes

3. Nesting Navigators

4. Hero Animations [Shared Element Transition]


Popup routes

Routes don't have to obscure the entire screen. PopupRoutes cover the screen with a ModalRoute.barrierColor that can be only partially opaque to allow the current screen to show through. Popup routes are "modal" because they block input to the widgets below.

There are functions which create and show popup routes. 

For example: 

showDialog, showMenu, and showModalBottomSheet. These functions return their pushed route's Future as described above. Callers can wait for the returned value to take an action when the route is popped, or discover its value.

There are also widgets which create popup routes, like PopupMenuButton and DropdownButton. These widgets create internal subclasses of PopupRoute and use the Navigator's push and pop methods to show and dismiss them.

Custom routes

You can create your own subclass of one of the widget library route classes like PopupRoute, ModalRoute, or PageRoute, to control the animated transition employed to show the route, the color and behaviour of the route's modal barrier, and other aspects of the route.

The PageRouteBuilder class makes it possible to define a custom route in terms of callbacks.

//navigate using PageRouteBuilder

  void _navigateToHeroAnimationPage(String appTitle) {

    //The PageRouteBuilder class is used to create custom route transitions.

    //PageRouteBuilder provides an Animation object.

    //This Animation can be used with Tween and Curve objects to customize the transition animation.


    Navigator.of(context).push(

      PageRouteBuilder(

        transitionDuration: Duration(milliseconds: 1000),

        pageBuilder: (BuildContext context, Animation<double> animation,

            Animation<double> secondaryAnimation) {

          return HeroAnimationPage(title: appTitle);

        },

        transitionsBuilder: (BuildContext context, Animation<double> animation,

            Animation<double> secondaryAnimation, Widget child) {

          return Align(

            child: FadeTransition(

              opacity: animation,

              child: child,

            ),

          );

        },

      ),

    );

  }


Nesting Navigators

An app can use more than one Navigator. Nesting one Navigator below another Navigator can be used to create an "inner journey" such as tabbed navigation, user registration, store checkout, or other independent journeys that represent a subsection of your overall application.

Example:

While using tabLayout where each tab maintains its own navigation history. Therefore, each tab has its own Navigator, creating a kind of "parallel navigation".

In addition to the parallel navigation of the tabs, it is still possible to launch full-screen pages that completely cover the tabs. For example: an on-boarding flow, or an alert dialog. Therefore, there must exist a "root" Navigator that sits above the tab navigation. As a result, each of the tab's Navigators are actually nested Navigators sitting below a single root Navigator.

Hero Animations [Shared Element Transition]

Flying an image from one screen to another is called a hero animation in Flutter, though the same motion is sometimes referred to as a shared element transition.

Basic structure of a hero animation

  • Use two hero widgets in different routes but with matching tags to implement the animation.
  • The Navigator manages a stack containing the app’s routes.
  • Pushing a route on or popping a route from the Navigator’s stack triggers the animation.
  • The Flutter framework calculates a rectangle tween, RectTween that defines the hero’s boundary as it flies from the source to the destination route. During its flight, the hero is moved to an application overlay, so that it appears on top of both routes.

Popular 3rd party libraries used for Navigation

1. Fluro

2. GetX

3. Sailor

4. Flutter-Deep-Link-Navigation

Fluro

Fluro is a Flutter routing library that adds flexible routing options like wildcards, named parameters and clear route definitions.

Features

  • Simple route navigation
  • Function handlers [map to a function instead of a route]
  • Wildcard parameter matching
  • Querystring parameter parsing
  • Common transitions built-in
  • Simple custom transition creation

Example:

router.navigateTo(context, "/users/1234", transition: TransitionType.fadeIn);

GetX

GetX is an extra-light and powerful solution for Flutter. It combines high performance state management, intelligent dependency injection, and route management in a quick and practical way.

GetX has 3 basic principles

  • Performance: GetX is focused on performance and minimum resource consumption.
  • Productivity: GetX uses a pleasant and easy syntax.
  • Organization: GetX allows total decoupling of the View from the business logic.


Easy syntax: Get.to(OtherSereen())

Sailor

A Flutter package for easy navigation management.

Features

  • Passing Parameters
  • Passing Arguments
  • Route Guards [Experimental]
  • Transitions
  • Pushing Multiple Routes
  • Log Navigation

Flutter Deep Link Navigation

Provides an elegant abstraction for complete deep linking navigation in Flutter.

This package only provides deep linking for internal navigation. Any external platform-level deep linking solutions can be used optionally in conjunction with this package.