Navigator 2.0: Navigation & Routing In Flutter

Introduction:

​ This article will explain the basics of the new Navigator API in Flutter, that is Navigator 2.0. ​

Table Of Contents:

  • Gaps in Navigator 1.0.
  • Requirements to bridge those gaps.
  • Navigator 2.0.
  • How does Navigator 2.0 work?
  • Drawbacks Of Navigator 2.0
  • Conclusion

    Gaps In Navigator 1.0

  • It is imperative: It follows the opposite of the declarative approach, which means that you have to define all the objects or the states in the widgets and the widgets will display whatever content you ask it to display. Moreover, if someone wants to remove some routes in the middle, doing so through an imperative approach wouldn't be very easy and modifying the imperative approach would not scale for a lot of use cases because usually, one API fits for a single use case. ​ Imp vs Dec
  • Access to the route stack: The route stack can not be accessed inside Navigator 1.0.
  • Operating System Events: Navigator 1.0 does not have a good way of handling the operating system event. As an example. let's say a user presses the back key on Android and the developer has a multiple navigator. There is no good way to navigate through multiple routes.
  • Web Support: Navigator 1.0 does not have good web support, as the forward button does not work, the back button seldom works and if it is pressed too many times, the code breaks. ​

    Requirements To Bridge Those Gaps.

Solved ​ In my opinion, two simple implementations can bridge the gaps that exist in the first iteration of Navigator:

  • A Declarative model, that user can have access with and can modify the route stack freely, and...
  • A systematic way to handle more operating system events. ​
    Navigator 2.0 is a declarative API which sets the history stack of the Navigator and has a Router widget to configure the Navigator based on app state and system events.

Navigator 2.0 has been deemed as 'complicated' by a lot of people, and rightfully so, owing to the massive boilerplate that it generates, but Navigation 2.0 actually makes life a lot easier and has a better implementation for the infamous 'Android back' button.

NOTE : The complete source code for the following article is present here

How does Navigator 2.0 work?

Navigator 2.0 consists of two APIs: ​

  • Page API
  • Router API

    Page API

Page API is a new declarative API in the navigator widgets. It defines the routes and the pages that are used in the app.

The Navigator widget has two new arguments passed in its constructor :

  • pages (Page Class): It is a list of pages where each page essentially describes the configuration for a Route .
  • onPopPage: A callback function for the imperative pop.

Page Class

​ A page creates a route that gets placed on the route stack. Each page takes a key so that when Navigator uses this page to update, it could differentiate between the pages. ​ For instance: If the navigator already has an existing list and you give it a new list, then the associated key is used to decide if the page in the new list is corresponding to a certain page in the old list or not. Then, you can decide to update, pop, re-push or perform any action on it.

Hence, the navigator will be able to differentiate between pages. ​

Backwards Compatibility

You can use Page API and navigator push and pop at the same time as Page API is backwards compatible, which also means that you can use it with imperative API.

Define a global key for the NavigatorState and use it while building the Navigator. ​

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

​ Use the above Global Key in the Navigator constructor and provide the list of pages along with the routes of the application in the page class. ​

Navigator(
    key: navigatorKey,
    pages:  <Page<void>>[
        MaterialPage(key: ValueKey('home'), child: MyHomePage()),
        if (_myRoute ==  MyRoute.gallery)
        MaterialPage(key: ValueKey('gallery'), child: GalleryPage(_tab)),
        if (_myRoute ==  MyRoute.seeAll)
        MaterialPage(key: ValueKey('seeAll'), child: MyLinkPage()),
        if (_myRoute ==  MyRoute.more)
        MaterialPage(key: ValueKey('seeAll/more'), child: ContentDetail()),
    ],
    onPopPage: _handlePopPage,
)

OnPopPage: ​ Whenever you use the Page API, you must provide this callback in order to handle the imperative pop on the page based route (route created by Page API) and according to the result that we get after calling router.didPop(), we can update the page. ​

bool  _handlePopPage(Route<dynamic> route, dynamic result) {
    final  bool success = route.didPop(result);
    if (success) {
        _myRoute =  MyRoute.home;
        notifyListeners();
    }
    return success;
}

Router API

This is a new widget that handles the operating system events and can change the configuration of the Navigator in response to them. Furthermore, it wraps a Navigator and configures its current list of pages based on the current app state.

=> The operating system events that can be handled by the Router API include: ​

  • Android back button intent.
  • Operating system set initial route -> is usually called when there is deep linking in iOS or Android.
  • Operating system push route -> is usually called when there is deep linking in iOS or Android.
  • URL update(in web app).

=> When the user directly types the URL, it will send that request as a push route or send an initial route intent to the Flutter Framework.

=> The router usually builds a Navigator with the Page API. This is where Router API and Page API meet. ​ To wire up all the events, the router will take in these delegates: ​ -RouteInformationProvider: Serves as a bridge to send the route information between the operating system and the router. Sends route Information back and forth from the OS to router.

MyConfiguration is the structure class for the whole application, as below:

class MyConfiguration {
    const  MyConfiguration(this.myRoute, this.tab);
    final  MyRoute myRoute;
    final  int tab;
}
  • RouteInformationParser: It parses the route information provided by the RouteInformationProvider and will hand the configuration to the router delegate.

parseRouteInformation will convert the given route information into parsed data to pass to RouterDelegate.

class  MyRouteInformationParser extends RouteInformationParser<MyConfiguration> {
    @override
    Future<MyConfiguration> parseRouteInformation(RouteInformation routeInformation) async {
        final  String routeName = routeInformation.location;
        if (routeName ==  '/')
            return  MyConfiguration(MyRoute.home, routeInformation.state);
        else  if (routeName ==  '/gallery')
            return  MyConfiguration(MyRoute.gallery, routeInformation.state);
        else  if (routeName ==  '/seeAll')
            return  MyConfiguration(MyRoute.seeAll, routeInformation.state);
        else  if (routeName ==  '/seeAll/more')
            return  MyConfiguration(MyRoute.more, routeInformation.state);
        throw  'unknown';
    }
    @override
    RouteInformation  restoreRouteInformation(MyConfiguration configuration) {
        switch (configuration.myRoute) {
            case  MyRoute.home:
                return  RouteInformation(location:  '/', state: configuration.tab);
            case  MyRoute.gallery:
                return  RouteInformation(location:  '/gallery', state: configuration.tab);
            case  MyRoute.seeAll:
                return  RouteInformation(location:  '/seeAll', state: configuration.tab);
            case  MyRoute.more:
                return  RouteInformation(location:  '/seeAll/more', state: configuration.tab);
        }
        throw  'unknown';
    }
}

-BackButtonDispatcher: It propagates the android back button to the router of your choice. It uses ChildBackButtonDispatcher to propagate to the sub-routers.

  • RouterDelegate: It is the center place to handle all incoming operating system events. It includes the parsed result from RouteInformationParser as well as the back button in turn from the BackButtonDispatcher and finally, it creates widgets mainly Navigator for the router subtree. ​ setNewRoutePath is called by the Router when the Router.routeInformationProvider reports that a new route has been pushed to the application by the operating system. ​

    class  MyRouterDelegate extends RouterDelegate<MyConfiguration> with  ChangeNotifier, PopNavigatorRouterDelegateMixin<MyConfiguration> {
      @override
      final  GlobalKey<NavigatorState> navigatorKey =  GlobalKey<NavigatorState>();
      MyRoute  get  myRoute => _myRoute;
      MyRoute _myRoute;
      set  myRoute(MyRoute value) {
          if (_myRoute == value) return;
          _myRoute = value;
          notifyListeners();
      }
      int get tab => _tab;
      int _tab =  0;
      set  tab(int value) {
          if (_tab == value) return;
          _tab = value;
          notifyListeners();
      }
    ​
      @override
      Future<void>  setNewRoutePath(MyConfiguration configuration) async {
          _myRoute = configuration.myRoute;
          _tab = configuration.tab ??  0;
      }
    ​
      @override
      MyConfiguration  get  currentConfiguration => MyConfiguration(myRoute, tab);
      bool  _handlePopPage(Route<dynamic> route, dynamic result) {
          final  bool success = route.didPop(result);
          if (success) {
              _myRoute =  MyRoute.home;
              notifyListeners();
          }
      return success;
      }
      @override
      Widget  build(BuildContext context) {
          return  Navigator(
              key: navigatorKey,
              pages:  <Page<void>>[
                  MaterialPage(key:  ValueKey('home'), child:  MyHomePage()),
                  if (_myRoute ==  MyRoute.gallery)
                      MaterialPage(key:  ValueKey('gallery'), child:  GalleryPage(_tab)),
                  if (_myRoute ==  MyRoute.seeAll)
                      MaterialPage(key:  ValueKey('seeAll'), child:  SeeAllPage()),
                  if (_myRoute ==  MyRoute.more)
                      MaterialPage(key:  ValueKey('seeAll/more'), child:  ContentDetail()),
              ],
              onPopPage: _handlePopPage,
          );
      }
    }
    

    Program Flow

    Program flow

The Router widget basically wraps up all the four different delegates and wires all the arrows together. The first program flow is from the left-hand side. The operating system will send the initial route to the RouteInformationProvider, which will then hand that initial route to the RouteInformationParser to parse it. Once the RouteInformationParser parses a route, it will send the result to the RouterDelegate which will set the initial callback and then receive the parsed result. After that, it will configure itself and at some point, the router will ask the RouterDelegate to build a Navigator. ​

The second program flow also starts from the operating system on the left-hand side, and if you see the top most arrow, there's a backButtonPush that corresponds to the back button on Android. It will be captured by the BackButtonDispatcher and at this point, the RouterDelegate will receive this intent and then it will update itself. Then, the router will ask the RouterDelegate to build a new navigator for it.

The third program flow is a backward arrow all the way from the router widget back to the operating system. This program flow is for web applications, where the router wants to update the URL when it detects that the route information might have changed as a result of the router rebuild. So, whenever the router rebuilds, it will ask RouterDelegate about its current configuration, which will provide that configuration as feedback to the RouteInformationParser. It will restore that configuration back to route information and send it back to RouteInformationProvider, which will send that back to the operating system to perform the update. ​

Additional Tips:

​ Navigator 2.0 implements the router URL update methods: ​

  1. RouterDelegate.currentConfiguration: Called by the Router when it detects a route information that may have changed as a result of rebuild. ​
  2. RouteInformationParser.restoreRouteInformation: Restores the route information from the given configuration. This may return null, in which case the browser history will not be updated. ​
  3. PlatformRouteInformationProvider: The browser will create entries whenever the URL changes. If you don't want that, use Router.navigate or Router.neglect.

All browser navigations will be sent to the Router through RouterDelegate.setNewRoute which is called by the Router when the Router.routeInformationProvider reports that a new route has been pushed to the application by the operating system. ​ Wow ​ ​

Drawbacks Of Navigator 2.0

​ Since Navigator 2.0 is fairly recent, there sure are some drawbacks in it that are worth a mention: ​

  • Firstly, it is quite complex and not very easy to grasp, like other Flutter concepts.
  • It involves more boilerplate code as compared to its previous version. ​

Conclusion

Navigator 2.0 has indeed solved all the problems that existed in Navigator 1.0. It has majorly solved the multiple navigator routing issue in Flutter web and has also managed to handle the operating system events quite well.

Thank you so much for reading, I hope this article has broadened your horizon about Flutter Navigator 2.0 and prompted you to give it a shot. ❤️

No Comments Yet