Flutter Navigator 2 navigation and routing system (I)

Flutter Navigator 2 navigation and routing system (I)



Information drawn from

This article explains how Flutter’s new Navigator and Router API works. If you follow Flutter’s open design docs, you might have seen these new features referred to as the Router widget. We’ll explore how these APIs enable more fine-tuned control over the screens in your app and how you can use it to parse routes.

These new APIs are not breaking changes, they simply add a new declarative API. Before Navigator 2.0, it was difficult to push or pop multiple pages, or remove a page underneath the current one. However, if you are happy with how the Navigator works today, you can keep using it in the same (imperative) way.

The Router provides the ability to handle routes from the underlying platform and display the appropriate pages. In this article, the Router is configured to parse the browser URL to display the appropriate page.

This article helps you choose which Navigator pattern works best for your app, and explains how to use Navigator 2.0 to parse browser URLs and take full control over the stack of pages that are active. The exercise in this article shows how to build an app that handles incoming routes from the platform and manages the pages of your app. The following GIF shows the example app in action:

medium img

If you’re using Flutter, you’re probably using the Navigator and are familiar with the following concepts:

Before Navigator 2.0, Routes were pushed and popped onto the Navigator’s stack with either named routes or anonymous routes. The next sections are a brief recap of these two approaches.

Anonymous routes

Most mobile apps display screens on top of each other, like a stack. In Flutter, this is easy to achieve by using the Navigator.

MaterialApp and CupertinoApp already use a Navigator under the hood. You can access the navigator using Navigator.of() or display a new screen using Navigator.push(), and return to the previous screen with Navigator.pop():

import 'package:flutter/material.dart';

void main() {
  runApp(Nav2App());
}

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) {
                return DetailScreen();
              }),
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('Pop!'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

When push() is called, the DetailScreen widget is placed on top of the HomeScreen widget like this:

navigator 1 puah

The previous screen (HomeScreen) is still part of the widget tree, so any State object associated with it stays around while DetailScreen is visible.

Named routes

Flutter also supports named routes, which are defined in the routes parameter on MaterialApp or CupertinoApp:

import 'package:flutter/material.dart';

void main() {
  runApp(Nav2App());
}

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (context) => HomeScreen(),
        '/details': (context) => DetailScreen(),
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/details',
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('Pop!'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

These routes must be predefined. Although you can pass arguments to a named route, you can’t parse arguments from the route itself. For example, if the app is run on the web, you can’t parse the ID from a route like /details/:id.

Advanced named routes with onGenerateRoute

A more flexible way to handle named routes is by using onGenerateRoute. This API gives you the ability to handle all paths:

onGenerateRoute: (settings) {
  // Handle '/'
  if (settings.name == '/') {
    return MaterialPageRoute(builder: (context) => HomeScreen());
  }
  
  // Handle '/details/:id'
  var uri = Uri.parse(settings.name);
  if (uri.pathSegments.length == 2 &&
      uri.pathSegments.first == 'details') {
    var id = uri.pathSegments[1];
    return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
  }
  
  return MaterialPageRoute(builder: (context) => UnknownScreen());
},

Here’s the complete example:

import 'package:flutter/material.dart';

void main() {
  runApp(Nav2App());
}

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: (settings) {
        // Handle '/'
        if (settings.name == '/') {
          return MaterialPageRoute(builder: (context) => HomeScreen());
        }

        // Handle '/details/:id'
        var uri = Uri.parse(settings.name);
        if (uri.pathSegments.length == 2 &&
            uri.pathSegments.first == 'details') {
          var id = uri.pathSegments[1];
          return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
        }

        return MaterialPageRoute(builder: (context) => UnknownScreen());
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/details/1',
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  String id;

  DetailScreen({
    this.id,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Viewing details for item $id'),
            FlatButton(
              child: Text('Pop!'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
    );
  }
}

class UnknownScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text('404!'),
      ),
    );
  }
}

Here, settings is an instance of RouteSettings. The name and arguments fields are the values that were provided when Navigator.pushNamed was called, or what initialRoute is set to.

The Navigator 2.0 API adds new classes to the framework in order to make the app’s screens a function of the app state and to provide the ability to parse routes from the underlying platform (like web URLs). Here’s an overview of what’s new:

The following diagram shows how the RouterDelegate interacts with the Router, RouteInformationParser, and the app’s state:

medium - navigator 2 howto

Here’s an example of how these pieces interact:

------------------------------------------------------------------------

Last update on 17 Aug 2022

---