Flutter Navigator 2.0 for Authentication and Bootstrapping — Part 2: User Interaction
By: Cagatay Ulusoy
Tags: Dart, Flutter, Authentication, Navigator, Flutter App Development

In the first part of this series, we had a short introduction to the Navigator 2.0 API and explained the sample apps that we will be building incrementally. In this article, we will introduce the Router widget and the Pages API. Then we will explain how to build a navigation stack according to the app state changes. We will focus on the following user interactions causing the app state changes:

Selecting a color and shape border type by pressing the buttons in the lists

Pressing the back button in the app bar

Pressing the system back button (Android only)

Before introducing the Router widget and the Pages API we will start with a quick recap about what we already had known before the introduction of the Navigator 2.0 API namely, the Overlay widget, the Navigator widget, and the Route class.

Overlay Widget

We have an Overlay widget in the deep down of the whole navigation system of a Flutter application. The Overlay widget is a special Stack widget that we use many times in our application’s widget tree. The children of the Overlay widget float on other widgets.

Overlay widget has a list of OverlayEntry objects. We provide the children widgets to the Overlay widget via the builder property of an OverlayEntry .

To insert or remove an OverlayEntry to the Overlay , we need to find the closest Overlay to the widget in the widget tree. In many cases, the closest Overlay is the one created by the Navigator widget. Hence, we don’t need to construct an Overlay widget in our application.

A method to show value indicator on slider widget

Navigator Widget

The Navigator widget is usually placed near the top of the widget tree. It manages the routes with a stack discipline. Each Route has its own list of OverlayEntry objects which are managed by the Overlay widget of the Navigator widget.

Until the introduction of the Navigator 2.0 API, the routes have been pushed and popped off the Navigator widget in an imperative way.

Route

Although many times we say everything is a widget in Flutter, Route is not a widget. It is an entry managed by the Navigator widget. In Flutter Framework, we can group two types of Route : routes that replace entire screen on transition and pop up routes whose widgets overlay on the previous route.

Routes can have RouteSettings object which might have a name (otherwise anonymous) and arguments which is sometimes useful.

As mentioned before, each Route has its own list of OverlayEntry objects. Let’s say we have a tab bar or bottom navigation bar on our home page. If we add an OverlayEntry in one of the tabs, it will still be visible when you switch between tabs because the OverlayEntry is added to the current route’s OverlayEntry list on the top of the home screen. We should either add and remove the OverlayEntry when we switch between tabs or have a separate route for each tab in the home screen.

routes in Flutter Framework

Pages

In the imperative API, the Route objects are handled by calling the static methods of the Navigator widget such as push , pop , replace .

The declarative API introduces the Page class. As developers, our responsibility is to provide a list of Page objects to the Navigator widget in a stack discipline. Then, the Navigator widget converts the Page objects into the Route objects. Similar to the Route class, Page class is also not a widget but a class that extends the RouteSettings class.

The navigation stack is built based on the order of the Page objects in the list. When the list changes, an update to the navigation stack is triggered. Note that every Page object has a corresponding Route object. However, if the route is instantiated within the static imperative methods, it won’t have a Page object.

List of pages provided to Navigator widget

We can also customize the classes in the page list by extending the Page class. In the sample apps of this series, all the Page classes are customized.

The Navigator widget uses the key property of a Page object to determine if each Page in the list is the same or different than the already inflated Page in the corresponding Route . If the key is different or the Page does not exist in the list, createRoute method of the Page class is called.

In our sample app, the uniqueness of the Color page is defined by the color code, and the uniqueness of the Shape page is defined by the combination of the color code and the shape border type.

RouteInformation

RouteInformation is a data holder that contains information for a route. It has two fields: location and state . The location field is equivalent to a URL string and state field holds information about the application’s state for that route.

As we will see in the Web part of this series, RouteInformation object is internally used in the Router widget to communicate with the OS.

Router Widget

The Router widget is the brain of the navigation. It wraps the Navigator widget and configures the navigation history based on user interaction with the app, system back button press, initial route on app launch, and new intents by the operating system (OS).

The Router widget delegates its tasks to its components which we will be exploring each of them throughout this series:

RouterDelegate is responsible for building the Navigator widget according the app state and handling the pop requests. According to the Flutter team, this delegate is the heart of the Router widget. It makes sense considering that a Router cannot be instantiated with without a heart, as human cannot live without heart.

is responsible for building the widget according the app state and handling the pop requests. According to the Flutter team, this delegate is the heart of the widget. It makes sense considering that a cannot be instantiated with without a heart, as human cannot live without heart. RouteInformationParser delegate is responsible for parsing route information coming from the OS so that RouterDelegate can update the app state. It also handles restoring route information according to app state changes so that OS stays up to date on the navigation history. I consider delegate this as arms because a human can live without an arm, but still would feel the lack of it in daily life. Similarly, we can instantiate a Router widget without a RouteInformationParser but we will lack a complete Web application experience when we can’t restore and parse URLs on the address bar of we browsers.

delegate is responsible for parsing route information coming from the OS so that can update the app state. It also handles restoring route information according to app state changes so that OS stays up to date on the navigation history. I consider delegate this as arms because a human can live without an arm, but still would feel the lack of it in daily life. Similarly, we can instantiate a widget without a but we will lack a complete Web application experience when we can’t restore and parse URLs on the address bar of we browsers. RouteInformationProvider delegate handles creating a route information from the OS intents. For example, when we type on the address bar of the Web browser, the task of the RouteInformationProvider is interpreting the URL to an entity that the Router widget uses internally with its delegates. This delegate is more like a mouth and ear because it is the ear of the Router widget that interprets the new intent signals from the OS, and mouth that tells the updated route information back to the OS when app state changes.

delegate handles creating a route information from the OS intents. For example, when we type on the address bar of the Web browser, the task of the is interpreting the URL to an entity that the widget uses internally with its delegates. This delegate is more like a mouth and ear because it is the ear of the widget that interprets the new intent signals from the OS, and mouth that tells the updated route information back to the OS when app state changes. BackButtonDispatcher delegate is responsible for reporting the system level pop events on platforms that has platform back button or gestures. Again, this delegate is more like an ear that listen for the signals coming from the OS.

There are two ways of using the Router widget within the WidgetsApp :

Using WidgetsApp.router constructor, we pass the Router widget delegates as constructor parameters. Note that, using this method requires passing the RouteInformationParser delegate as constructor parameter. In the first three samples, we won’t be parsing and restoring routes. Therefore, we won’t be using this method until the fourth sample app. Instantiating the Router widget and passing it as the home property of the WidgetApp . In this case, the RouterDelegate should be non-null . We don’t have to provide other delegates when constructing the Router widget unless we want to customize the default behaviours RouteInformationProvider or utilize the capabilities of the RouteInformationParser and the BackButtonDispatcher .

Injecting Router to the App

Widget tree

Router Delegate

The most important component of the Router widget is the RouterDelegate class since it tells to the Router widget how to build the Navigator widget. If we are using the Navigator 2.0 API and the Router widget, our responsibility is customizing the RouterDelegate to implement the navigation logic.

The RouterDelegate class implements ChangeNotifier mixin which makes itself listenable. Calling the notifyListeners method inside RouterDelegate will notify the Router widget.

class implements mixin which makes itself listenable. Calling the method inside will notify the widget. After being notified, the Router calls the build() method of the RouterDelegate .

calls the method of the . RouterDelegate constructs and returns a Navigator widget to the Router within the build() method.

Calling notifyListeners is not the only way of building a navigation history. In this article by Lulupointu, you can see an example of handling the navigation state with Bloc pattern.

A very typical way but not only way of listening to the app state in the RouterDelegate class is passing callback methods to the pages. Inside the widgets of a page, when the callback method is triggered with user interaction, the Router widget is notified by the RouterDelegate for a navigation stack update. Note that as the application evolves, number of callback methods passed to the child widgets would increase. It would be overwhelming to keep track of this callback methods. We should always seek for good architecture patterns to avoid this risk.

We also need to update the app state inside RouterDelegate by responding to the operating system events. When the operating system requests to pop the current route, we need to figure out how to update the app state inside the RouterDelegate and rebuild a new navigation stack accordingly.

Now, let’s analyze our custom RouterDelegate for the first sample. The app state inside the customized RouterDelegate is represented with two fields: selectedColorCode and selectedShape .

If the selectedColorCode and selectedShape fields are both null (not set), then we should be in the HomePage . There is nothing else in the navigation stack. If the operating system is telling that the current route should be popped (by a back button press for example), the navigation stack will be empty and the entire app will be popped.

and fields are both (not set), then we should be in the . There is nothing else in the navigation stack. If the operating system is telling that the current route should be popped (by a back button press for example), the navigation stack will be empty and the entire app will be popped. If the selectedColorCode is set, and selectedShape is not set, it means that we clicked a color button in the list. The navigation history will include HomePage and the ColorPage . The order of the page list matters in a way that the last page in the list will be the current page visible to the user. When the back button is pressed, the ColorPage will be popped, and the HomePage will be visible.

is set, and is not set, it means that we clicked a color button in the list. The navigation history will include and the . The order of the page list matters in a way that the last page in the list will be the current page visible to the user. When the back button is pressed, the will be popped, and the will be visible. If the selectedColorCode and the selectedShape are both set, it means that we selected a color code and a shape border type for that color. The navigation stack will include the HomePage , ColorPage , and the ShapePage . When the back button is pressed, the last page in the list which is the ShapePage will be popped, and the app will show the ColorPage .

Note that we implement getter and setter for _selectedColorCode and _selectedShape states. The setter methods call notifyListeners so that when the values for these states are updated, the Router widget is notified. However, we don’t always have to notify the Router widget each time the state is set. For this case, it works and makes the state handling easier. Sometimes we may need to skip or delay the notifying so it doesn’t make sense to add the notifyListeners call to the setters.

Handling Pop Requests

When The Router widget receives a pop request from the operating system, it delegates the responsibility of updating the navigation stack to the RouterDelegate by calling its popRoute the method.

If the RouterDelegate wants to handle the pop event, this method should return true . If this method returns false , the entire app will be popped.

The RouterDelegate may implement the PopNavigatorRouterDelegateMixin . When this mixin is implemented, The Router widget’s popRoute call will invoke the maybePop method of the Navigator widget. In this case, we don’t need to override the popRoute method in the RouterDelegate . Instead, we must provide onPopPage callback to the Navigator widget as a constructor parameter.

We need to handle popping for ShapePage or ColorPage . When a pop event is received:

If the selectedShape is null , then we are currently showing the ColorPage which needs to be popped. Thus, we will clear the selectedColorCode state which will notify the Router widget.

is , then we are currently showing the which needs to be popped. Thus, we will clear the state which will notify the widget. If the selectedShape is non-null , then the visible page is the ShapePage and it needs to be popped. We should set only the selectedShape to null because we want to show the ColorPage with the selectedColorCode after popping the ShapePage .

Before handling the pop event, the RouterDelegate should make sure that the route actually is popped. If the route was not popped, the onPopPage should return false which tells to the Router widget that RouterDelegate does not care about the popping event.

The onPopPage callback method is called in the following cases:

BackButton Press:

The BackButton widget potentially causes popping the route when pressed. We usually don’t need to add a specific widget to pop the last route from the navigation stack since the Scaffold widget includes a BackButton within the AppBar widget. This widget adapts to the operating system meaning that its icon will be operating system specific. If the Navigator widget contains more than one page, the BackButton will be visible in the AppBar widget.

System back button press:

Unlike iOS phones and desktop apps, Android phones have a dedicated system back button or gestures to pop the last route. If we want the Router widget to receive the pop requests coming from the operating system, we should provide a BackButtonDispatcher delegate to the Router widget.

In the recording below we see that the entire app is popped when the pop request comes from the Android operating system since we don’t provide a back button dispatcher delegate to the Router widget

Now let’s provide RootBackButtonDispatcher to Router widget. This class is the default implementation of the back button dispatcher.

In the screen recording below, we now see the expected behavior since onPopPage callback is invoked when the system back button is tapped in an Android phone.

As we will discuss in more detail in the Web part of this series, pressing the back button on the Web browser doesn’t invoke the onPopPage callback.

Explicit pop call:

Using the static imperative pop , maybePop , popUntil , popAndPushNamed , restorablePopAndPushNamed methods of the Navigator widget also causes popping the last route in the navigation stack.

Conclusion

In this article, we had an introduction to the declarative navigation API and started with a very simple navigation logic. You can find the source code on the Github page. The project includes multiple main.dart files. The easiest way of running the sample app is right-clicking on the main_002_01.dart file and clicking the Run command.

In the next article, we add the authentication use case to this sample app. Special thanks to Jon Imanol Durán who reviewed all the articles in this series and gave me useful feedback. If you liked this article, please press the clap button, and star the Github repository.