To prepare

Read our previous article about the principles of Flutter routingSource Code analysis of Flutter RoutingNavigator2.0.

To demonstrate the use of Navigator2.0, a simple example is prepared here for project download.

The nav_demo directory is an example using Navigator1.0 with four pages: splash, login, home, and Details

The code structure is as follows:

Core code:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'the Navigator 1.0',
      routes: <String, WidgetBuilder> {
        '/login': (_) = >const Login(),
        '/home': (_) = >const Home(),
      },
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: constSplash(), ); }}Copy the code

Navigator1.0 has several problems:

  • Static routes are not as flexible as dynamic routes because they do not easily pass parameters to constructors. If the third-party routing framework is not considered, static and dynamic routes are often used together. Click on the grid TAB to pull up the details page

    GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute( builder: (ctx) => Details( _movieList! [i].name, _movieList! [i].imgUrl))); }, child: Column( mainAxisSize: MainAxisSize.min, children: [ Flexible(child: Image.network(_movieList![i].imgUrl)), Text(_movieList![i].name), ], ), );Copy the code
  • Difficult to deal with the Web version, address bar URL routing navigation requirements. As shown in the figure below, route is switched and the address bar remains unchanged

  • The control of routing is very inflexible, for example, the scene requirements of routing nesting; Handle Android return key requirements, etc

  • Different programming styles. Navigator 1.0 is an imperative programming paradigm, while Flutter itself is a declarative programming paradigm. Navigator 2.0 is a return to the declarative paradigm, with more of a Flutter flavor.

Refactoring using Navigator 2.0

Here, I’ll start by refactoring in a minimally modified, simplest way to use Navigator 2.0

Modify the code structure:

Add only the Router folder and the delegate.dart file. In which the custom class MyRouterDelegate inherited from RouterDelegate, and mixed with ChangeNotifier and PopNavigatorRouterDelegateMixin.

There are three methods that must be implemented:

class MyRouterDelegate extends RouterDelegate<List<RouteSettings>> with ChangeNotifier.PopNavigatorRouterDelegateMixin<List<RouteSettings>> {

  final List<Page> _pages = [];

  @override
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: List.of(_pages),
      onPopPage: _onPopPage,
    );
  }

  @override
  Future<void> setNewRoutePath(List<RouteSettings> configuration) async {}

/// ........................... Omit some code ………………
}
Copy the code

The setNewRoutePath method can be left blank, focusing on the build method. We created Navigator as the manager of the route and set two main parameters: Pages and onPopPage. Pages is a list of Page objects. When a route is popped, the onPopPage is called back and the developer can handle the logic of the route being pushed back.

What we really need to care about here is,PageWhat is an object list?

We know that in Flutter, the word route is used to refer to pages in App, and the routing stack is the page stack. The Page class, introduced in 2.0, is essentially a route description file. This idea is similar to the four trees of A Flutter that I mentioned in the Realization of the Flutter Framework. The Widget in Flutter is a configuration description from which the Element class is generated. Similarly, Page is a description used to generate the actual routing object.

Understand this, and you’ll see that Navigator2.0 is simpler, not more complicated, to manage. As soon as we manipulate the Page list, the corresponding routing stack will sense it and automatically change. We simply place the page we want to display at the last element of the List. Navigator2.0 turns the black box of routing stacks into a List of lists. We want to change the order of pages in the routing stack by simply changing the position of the elements in List .

Next we need to look at how to create objects using the Page class. The Page class itself descends from the RouteSettings class, indicating that it is indeed a routing configuration file. It is an abstract class and cannot be instantiated. We found two classes that directly implement it:

We can see that Flutter already provides a Material style for Android and a Cupertino style for iOS implementation classes.

Wrapper the Page we wrote directly with the MaterialPage, adding a method to encapsulate the logic to help create the Page:

  MaterialPage _createPage(RouteSettings routeSettings) {
    Widget child;

    switch (routeSettings.name) {
      case '/home':
        child = const Home();
        break;
      case '/splash':
        child = const Splash();
        break;
      case '/login':
        child = const Login();
        break;
      case '/details':
        child = Details(routeSettings.arguments! as Map<String.String>);
        break;
      default:
        child = const Scaffold();
    }

    return MaterialPage(
      child: child,
      key: Key(routeSettings.name!) as LocalKey,
      name: routeSettings.name,
      arguments: routeSettings.arguments,
    );
  }
Copy the code

The processing here is somewhat similar to the static routing configuration table, but note that we can add the RouteSettings parameter to the page constructor, which is much more flexible than the 1.0 static routing.

Well, there are only a few ways to manipulate the Page list:

/// Press into a new page to display
void push({required String name, dynamic arguments}) {
    _pages.add(_createPage(RouteSettings(name: name, arguments: arguments)));
    // Notify the routing stack that our Page list has been changed
    notifyListeners();
  }

/// Replace the page that is currently being displayed
void replace({required String name, dynamic arguments}) {
    if (_pages.isNotEmpty) {
      _pages.removeLast();
    }
    push(name: name,arguments: arguments);
  }
Copy the code

Finally, you need to make changes to main.dart in order to use the Navigator2.0 interface. Here, we instantiate a MyRouterDelegate global variable in app.dart for simplicity:

import 'package:nav2_demo/router/delegate.dart';

MyRouterDelegate delegate = MyRouterDelegate();
Copy the code

Alter main.dart to reference the global variable directly:

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key) {
    // Add the first page at initialization
    delegate.push(name: '/splash');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'the Navigator 2.0',
      debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: Router( routerDelegate: delegate, backButtonDispatcher: RootBackButtonDispatcher(), ), ); }}Copy the code

Create a Router declaratively and set the routerDelegate attribute. BackButtonDispatcher is not required here, but our example creates a default implementation of RootBackButtonDispatcher() to demonstrate the handling of the return key.

Okay, we’re almost done. Replace all the places where we used to operate the routing stack with Navigator1.0 with our own interface.

// Display the home page
delegate.replace(name: '/home');
Copy the code

Including where we passed arguments to the detail page constructor using dynamic routing earlier:

/// home.dart

GestureDetector(
   onTap: (){
     delegate.push(name: '/details',arguments: {'name':_movieList! [i].name,'imgUrl':_movieList! [i].imgUrl}); }, child: Column( mainAxisSize: MainAxisSize.min, children: [ Flexible(child: Image.network(_movieList![i].imgUrl)), Text(_movieList![i].name), ], ), );Copy the code

Details page receive data:

class Details extends StatelessWidget {

  final String name;
  final String imgSrc;
  
  Details(Map<String.String> arguments)
      : name = arguments['name']! , imgSrc = arguments['imgUrl']! ;/// Omit some code
}
Copy the code

Can you see that? We can now easily pass parameters to page constructs in a static routing manner similar to 1.0.

This is the basic use of Navigator2.0. Is it still hard to understand?

Here’s the complete code, and we’ve also rewritten the popRoute method to handle the logic of the page exit. Most of the time, we do not want the user to directly exit the App when clicking the back button. The following is done. When the user has returned to the root routing page, a dialog box will pop up asking the user whether he is sure to exit the App. The processing in 1.0 was very inelegant and needed to be wrapped by WillPopScope. Now it is no longer needed and can be handled directly in popRoute.

class MyRouterDelegate extends RouterDelegate<List<RouteSettings>> with ChangeNotifier.PopNavigatorRouterDelegateMixin<List<RouteSettings>> {

  final List<Page> _pages = [];

  @override
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: List.of(_pages),
      onPopPage: _onPopPage,
    );
  }

  @override
  Future<void> setNewRoutePath(List<RouteSettings> configuration) async {}

  @override
  Future<bool> popRoute() {
    if (canPop()) {
      _pages.removeLast();
      notifyListeners();
      return Future.value(true);
    }
    return _confirmExit();
  }


  bool canPop() {
    return _pages.length > 1;
  }

  bool _onPopPage(Route route, dynamic result) {
    if(! route.didPop(result))return false;

    if (canPop()) {
      _pages.removeLast();
      return true;
    } else {
      return false; }}void push({required String name, dynamic arguments}) {
    _pages.add(_createPage(RouteSettings(name: name, arguments: arguments)));
    notifyListeners();
  }

  void replace({required String name, dynamic arguments}) {
    if (_pages.isNotEmpty) {
      _pages.removeLast();
    }
    push(name: name,arguments: arguments);
  }

  MaterialPage _createPage(RouteSettings routeSettings) {
    Widget child;

    switch (routeSettings.name) {
      case '/home':
        child = const Home();
        break;
      case '/splash':
        child = const Splash();
        break;
      case '/login':
        child = const Login();
        break;
      case '/details':
        child = Details(routeSettings.arguments! as Map<String.String>);
        break;
      default:
        child = const Scaffold();
    }

    return MaterialPage(
      child: child,
      key: Key(routeSettings.name!) as LocalKey,
      name: routeSettings.name,
      arguments: routeSettings.arguments,
    );
  }

  Future<bool> _confirmExit() async {
    final result = await showDialog<bool>( context: navigatorKey.currentContext! , builder: (context) {return AlertDialog(
            content: const Text('Are you sure you want to quit the App? '),
            actions: [
              TextButton(
                child: const Text('cancel'),
                onPressed: () => Navigator.pop(context, true),
              ),
              TextButton(
                child: const Text('sure'),
                onPressed: () => Navigator.pop(context, false),),,); });return result ?? true; }}Copy the code

Use in-depth

In the above case, we still haven’t solved the Web version problem. When we enter the URL in the browser address bar, we cannot locate the specific routing page. When we switch to a specific routing page, the URL of the address bar does not change synchronously. If your application is going to be web-compatible in the future, it’s important to continue learning more about Navigator2.0.

To handle this, we need to customize a routing information resolver:

/// parser.dart

class MyRouteInformationParser extends RouteInformationParser<List<RouteSettings>> {

  const MyRouteInformationParser() : super(a);@override
  Future<List<RouteSettings>> parseRouteInformation(RouteInformation routeInformation) {
    final uri = Uri.parse(routeInformation.location!) ;if (uri.pathSegments.isEmpty) {
      return Future.value([const RouteSettings(name: '/home')]);
    }

    final routeSettings = uri.pathSegments
        .map((pathSegment) => RouteSettings(
              name: '/$pathSegment',
              arguments: pathSegment == uri.pathSegments.last
                  ? uri.queryParameters
                  : null,
            ))
        .toList();

    return Future.value(routeSettings);
  }

  @override
  RouteInformation restoreRouteInformation(List<RouteSettings> configuration) {
    final location = configuration.last.name;
    final arguments = _restoreArguments(configuration.last);

    return RouteInformation(location: '$location$arguments');
  }

  String _restoreArguments(RouteSettings routeSettings) {
    if(routeSettings.name ! ='/details') return ' ';
    var args = routeSettings.arguments as Map;

    return '? name=${args['name']}&imgUrl=${args['imgUrl']}'; }}Copy the code

There are two methods that need to be implemented, respectively

  • ParseRouteInformation: Helps us translate a URL address into a routing state (configuration information)

  • RestoreRouteInformation: Helps us convert the state (configuration information) of a route to a URL address

It can be seen that the functions of these two methods are just opposite, and exactly correspond to our two requirements: enter the URL to switch the corresponding routing page; Operation routing page, URL synchronization changes.

Specifically, the parseRouteInformation method receives a RouteInformation type parameter that describes information about a URL containing two properties, the string location and the dynamic type state. Location is the path part of the URL, and state is used to save the state in the page. For example, there is an input box in the page, and the input content in the input box is saved in state. When the page is restored next time, the data can also be restored. Knowing the parameters to this method, the code implementation above is easy to understand. It’s easier to parse the URL’s path to a Uri than to manipulate the string path directly, and then generate the corresponding routing configuration RouteSettings and return it.

The restoreRouteInformation method is simpler in that it takes a set of route configurations as parameters, and I need to combine them to generate a URL that I return as a RouteInformation object. The URL returned here is the URL used to update the browser’s address bar.

Now that we have our routing information parser in place, we need to add code to MyRouterDelegate:

  @override
  List<Page> get currentConfiguration => List.of(_pages);

  @override
  Future<void> setNewRoutePath(List<RouteSettings> configuration) {
    debugPrint('setNewRoutePath ${configuration.last.name}');

    _setPath(configuration
        .map((routeSettings) => _createPage(routeSettings))
        .toList());
    return Future.value(null);
  }

  void _setPath(List<Page> pages) {
    _pages.clear();
    _pages.addAll(pages);

    if(_pages.first.name ! ='/') {
      // _pages.insert(0, _createPage(const RouteSettings(name: '/')));
    }
    notifyListeners();
  }
Copy the code

First we need to override the currentConfiguration get method, which returns our Page list, and then implement the setNewRoutePath method that we left empty.

The call to parseRouteInformation, which was implemented earlier in the routing information parser, is followed by a callback to the setNewRoutePath method here, and obviously, The return value of the parseRouteInformation method is the parameter that is forwarded to the setNewRoutePath method. We’ve parsed the URL in the parseRouteInformation method and generated a set of routing configuration information, which is now forwarded to setNewRoutePath, which means that in setNewRoutePath, Generate the corresponding Page object of this group of routing configuration information, and insert it into the current Page list, and finally realize the routing stack update. The whole process can be summarized in one sentence, that is, a URL input from the outside, which eventually leads to the generation and update of the routing page in the App.

Finally, modify main.dart and set our routing information resolver:

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

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key) {
    delegate.push(name: '/splash');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'the Navigator 2.0',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routerDelegate: delegate,
      routeInformationParser: constMyRouteInformationParser(), ); }}Copy the code

Here, we directly replace the router, the new constructor provided by the MaterialApp. Use this constructor to omit the RootBackButtonDispatcher we set earlier.

Switch App route, the address bar is also updated synchronously:

Enter the URL to navigate to the corresponding route within the App:

Perfect! Full source code access nav2_demo

Use the summary

  • implementationRouterDelegate: it is a proxy for the route, and we must implement this class to manage the route using the Navigator 2.0 interface. In fact, it’s also an observed, it’s managed internallyPageList, only modifiedPageLists do not cause real updates to the underlying routing stack, so we need to blend inChangeNotifier, the operation is finishedPageAfter the list, also callnotifyListeners()Notifies the observer of the data change, triggering a real update of the underlying routing stack. And in our case code, it’s mixed inPopNavigatorRouterDelegateMixin“, mainly to rewrite itpopRouteMethod, custom unstack logic.
  • implementationRouteInformationParser: it is a routing information parser. Its main purpose is to handle urls.

Note that Navigator 2.0’s interface works better with the state management framework, which is not incorporated in this article for simplicity.

Overall, Navigator 2.0 is not complex. It makes Flutter routing management more convenient and flexible. Based on this mechanism, third-party developers can build a highly encapsulated framework for remote and dynamic routing navigation.


Follow wechat official account: “The path of programming from 0 to 1”

Don’t be disappointed with the dry goods tutorial!

Or follow video lessons from bloggers