Flutter Navigator 2.0

Devanshi Garg's photo
Devanshi Garg
·Feb 1, 2021·

13 min read

Introduction:

​ This article will explain the basics of the new Navigator API in flutter that is Navigator 2.0. In this article, we will be covering the following topics: ​

  • Navigator 1.0 problems
  • Requirements to solve those problems
  • Navigator 2.0
  • How does Navigator 2.0 work
  • Disadvantages of Navigator 2.0 ​
  • It is imperative: that is it is the opposite of the declarative approach which means you have to define all the objects or the states in the widgets and the widgets will display whatever content you ask it to do. Moreover, if someone wants to remove let's say three routes in the middle. And doing so with the imperative approach wouldn't be very easy. And modifying the imperative approach would not scale for so many use cases because usually, 1 API fits for 1 use case. ​ Imp vs Dec
  • Access to the route stack: The route stack can not be accessed inside the Navigator 1.0
  • Operating System Events: Navigator 1.0 does not have a good way to handle the operating system event. Let's say a user presses the back key on android and the developer has a multiple navigator, there is not any good way to navigate through multiple routes.
  • Web Support: Navigator 1.0 does not have good web support, as forward button does not work, the back button works sometimes but if it is pressed too many times, the code will break. ​

    Requirements to solve the above problems:

    ​ ​ Solved
  • Declarative model: That user can have access, and they can modify the route stack freely.
  • A systematic way to handle more operating system events. ​ ​

    NOTE : The full source code for the following article is present here https://github.com/DevanshiGarg08/Navigation_2.0

Navigator 2.0 consists of two APIs: ​

  • Page API: This is a new declarative API in the navigator widgets.
  • Router API: This is a new widget that handles operating system events.

PAGE API

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 a new list. Then the key is used to decide if the new page in the list is corresponding to a certain page in the old list. Then you can decide to update the content or pop or re-push or whatever you want to. Hence the navigator will be able to design which page is which. ​

**You can use Page API and navigator push and pop at the same time as Page API is backward compatible I.E you can use it with imperative API. ​

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

​ ​

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 which we will 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:

The operating system events and web support were not being handled hence this API came into action. ​

=> Router API is a new widget that wires up the operating system events. ​ The operating system events include: ​

  • Android back button intent.
  • Operating system set initial route. -> usually calls when there is deep linking in iOS or android
  • Operating system push route. -> usually calls 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 os to router.
  • RouteInformationParser: It parses the route information provided by the RouteInformationProvider and it will hand the configuration to the router delegate.
  • 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. ​
    class MyConfiguration {
    const  MyConfiguration(this.myRoute, this.tab);
    final  MyRoute myRoute;
    final  int tab;
    }
    
    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';
    }
    }
    
    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

The router widget basically wraps up the entire 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. And then the RouteInformationProvider will 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 the RouterDelegate will 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 arrow there's a backButtonPush that's corresponding to Android back and then it will be captured by the BackButtonDispatcher. And at this point, the RouterDelegate receives this intent, and then it will update itself. And then at a certain point, 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 application when the router wants to update the URL when it detects 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, And then RouterDelegate will provide that configuration as feedback to the RouteInformationParser and RouteInformationParser will restore that configuration back to route information. And then it will send back to RouteInformationProvider and then RouteInformationProvider will send that back to the operating system to do the update. ​

How does Navigator 2.0 work?

  • Implements the router URL update methods ​
  • RouterDelegate.currentConfiguration: Called by the Router when it detects a route information may have changed as a result of rebuild. ​
  • RouteInformationParser.restoreRouteInformation: Restores the route information from the given configuration.This may return null, in which case the browser history will not be updated. ​
  • 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 ​ ​

    Disadvantages of Navigator 2.0:

    ​ So we are almost on the verge of our article, and there are some of the disadvantages of Navigator 2.0 which I would like to mention: ​
  • Firstly, it is quite complex and not very easy to grasp like other flutter topics.
  • It involves more boilerplate code as compared to its previous version. ​

    Conclusion:

    I would like to conclude this article by saying that Navigator 2.0 has indeed solved all the problems that were faced in Navigator 1.0. Navigator 2.0 has majorly solved the multiple navigator routing in flutter web. It 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
 
Share this