Flutter Navigator 2.0 for Authentication and Bootstrapping — Part 2: User Interaction
- Part 1: Introduction
- Part 2: User Interaction
- Part 3: Authentication
- Part 4: Bootstrapping
- Part 5: Web
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.
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.
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 theRoute
objects. Similar to theRoute
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.
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 theNavigator
widget according the app state and handling the pop requests. According to the Flutter team, this delegate is the heart of theRouter
widget. It makes sense considering that aRouter
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 thatRouterDelegate
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 aRouter
widget without aRouteInformationParser
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 theRouteInformationProvider
is interpreting the URL to an entity that theRouter
widget uses internally with its delegates. This delegate is more like a mouth and ear because it is the ear of theRouter
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 theRouter
widget delegates as constructor parameters. Note that, using this method requires passing theRouteInformationParser
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 thehome
property of theWidgetApp
. In this case, theRouterDelegate
should benon-null
. We don’t have to provide other delegates when constructing theRouter
widget unless we want to customize the default behavioursRouteInformationProvider
or utilize the capabilities of theRouteInformationParser
and theBackButtonDispatcher
.
Router Delegate
The most important component of the Router
widget is the RouterDelegate
class since it tells to theRouter
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 implementsChangeNotifier
mixin which makes itself listenable. Calling thenotifyListeners
method insideRouterDelegate
will notify theRouter
widget. - After being notified, the
Router
calls thebuild()
method of theRouterDelegate
. RouterDelegate
constructs and returns aNavigator
widget to theRouter
within thebuild()
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 theRouterDelegate
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
andselectedShape
fields are bothnull
(not set), then we should be in theHomePage
. 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, andselectedShape
is not set, it means that we clicked a color button in the list. The navigation history will includeHomePage
and theColorPage
. 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, theColorPage
will be popped, and theHomePage
will be visible. - If the
selectedColorCode
and theselectedShape
are both set, it means that we selected a color code and a shape border type for that color. The navigation stack will include theHomePage
,ColorPage
, and theShapePage
. When the back button is pressed, the last page in the list which is theShapePage
will be popped, and the app will show theColorPage
.
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 theNavigator
widget. In this case, we don’t need to override thepopRoute
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
isnull
, then we are currently showing theColorPage
which needs to be popped. Thus, we will clear theselectedColorCode
state which will notify theRouter
widget. - If the
selectedShape
isnon-null
, then the visible page is theShapePage
and it needs to be popped. We should set only theselectedShape
tonull
because we want to show theColorPage
with theselectedColorCode
after popping theShapePage
.
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 theRouter
widget to receive the pop requests coming from the operating system, we should provide a BackButtonDispatcher
delegate to theRouter
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.