Flutter Navigator 2.0 for Authentication and Bootstrapping — Part 3: Authentication

- Part 1: Introduction
- Part 2: User Interaction
- Part 3: Authentication
- Part 4: Bootstrapping
- Part 5: Web
In the second part of this series, we explored the Navigator 2.0 API, particularly the Router
widget, the Page
class, and the RouterDelegate
class. The first sample app handles the navigation between HomePage
, ColorPage
, and the ShapePage
. In this second sample, we add the authentication use case and build the navigation stack according to the authentication state changes. In addition to the _selectedColorCode
and the _selectedShape
properties, we add _loggedIn
property which holds the authentication state in the RouterDelegate
.

The AuthRepository
In this project, we don’t use an authentication service such as Firebase Auth. AuthRepository
class is used to mock the login and logout processes with 2 seconds delay, and saves the authentication state in the local cache using the shared preferences
library. When the app is launched next time, it remembers the last authentication state and shows the HomePage
or LoginPage
accordingly.
The App
When the app is launched, the Router
widget is instantiated and located at the top of the app widget tree as in the previous sample. In this sample, we also inject AuthRepository
to the app through AutViewModel
class using the Provider State Management pattern.
The RouterDelegate
In this sample app, we define three Pages
lists: _splashStack
, _loggedInStack
, and _loggedOutStack
. Depending on the app stateRouterDelegate
will return a Navigator
widget with one of these pages list.
When the app is launched for the first time:
- The
Router
widget and theRouterDelegate
are initialized. TheRouterDelegate
invokes the_init
call on initialization. - In the
_init
call,RouterDelegate
asks the login state from theAuthRepository
and waits until the result is returned. - Meanwhile, the
Router
widget calls thebuild
method of theRouterDelegate
. Since the_loggedIn
state has not yet been set, it isnull
. As a result, theRouterDelegate
will instantiate aNavigator
widget with a_splashStack
which only hasSplashPage
.

- Once the login state result is returned from the
AuthRepository
, theRouterDelegate
sets theloggedIn
state accordingly.
- Inside the setter method of the
loggedIn
statenotifyListeners()
method is called and theRouter
widget is notified.
- The
Router
widget asks theRouterDelegate
to build a newNavigator
widget with a new navigation stack based on the new app state. - Depending on the value of the
loggedIn
state,_loggedOutStack
or_loggedInStack
is passed to theNavigator
widget.
Login
The onLogin
callback which is defined in the RouterDelegate
is passed to the LoginPage
and from there to the LoginScreen
. The state management inside the LoginScreen
widget is handled with AutViewModel
class which has access to the AuthRepository
(Note: Check Provider State Management pattern if you are not familiar).

When the user presses the login button inside the LoginScreen
, the following steps happen:
AuthViewModel.login()
method is calledlogingIn
state inside theAuthViewModel
is set totrue
and theLoginScreen
is notified about the state change.LoginScreen
rebuilds and shows theInProgressMessage
widget which is a text centered in the screen.AuthRepository.login
method is called and the result is returned to theAuthViewModel
from theAuthRepository
with a delay.loggingIn
state is set tofalse
and theLoginScreen
is notified about the state change.- If the result is
true
, the login process is successful. TheonLogin
callback which is defined in theRouterDelegate
and passed down to theLoginScreen
widget is invoked.
Now let’s see what happens in the RouterDelegate
when the onLogin
callback is invoked:
- The
loggedIn
state is settrue
- Inside the setter method of
loggedIn
, theRouter
widget is notified by calling thenotifyListeners()
method. Router
widget calls the build method of theRouterDelegate
.RouterDelegate
constructs and returns a newNavigator
widget to theRouter
widget with the_loggedInStack
.
The _loggedInStack
has a list of pages that are in the same order with the same logic as in the previous sample app. The only difference is that onLogout
callback method is injected into each Page
as a constructor parameter.
Logout
In this sample app, a Floating Action Button (FAB) is added to HomeScreen
,ColorScreen
, and to ShapeScreen
. The onLogout
callback is passed down to the FAB widget through these screens.

When the user presses the floating action button, the following steps happen:
AuthViewModel.logout()
method is calledloggingOut
state inside theAuthViewModel
is set totrue
and theLogoutFab
widget is notified about the state change.LogoutFab
widget rebuilds and shows a floating action button with a progress bar.AuthRepository.logout()
async method is called and the result is returned from theAuthRepository
with a delay.loggingOut
state is set tofalse
and theLogoutFab
widget is notified about the state change.onLogout
callback which is defined in theRouterDelegate
and passed down to theLogoutFab
widget is invoked.
Now let’s see what happens in the RouterDelegate
when the onLogout
callback is invoked:
- The
loggedIn
state is set tofalse
. - In many apps, when a users log out, all the preferences and selections are cleared. In this sample app, we clear the remaining app states as well with the
_clear()
method. - Inside the setter method of
loggedIn
, theRouter
widget is notified by calling thenotifyListeners()
method. Router
widget calls the build method of theRouterDelegate
.RouterDelegate
constructs and returns a newNavigator
widget to theRouter
widget with the_loggedOutStack
.
Disclaimer
In this sample app, the RouterDelegate
is informed about the authentication state changes via callback methods that are injected into the widgets. I chose to use callback methods due to demonstration purposes and easier explanation but I would not recommend this as the number of these callback methods is subject to increase over time during application development. As a result, it will be overkill to inject each one of them into the widgets through constructor parameters. We need to be smart and apply the best practices with a clean architecture.
A better approach to react to the app state changes in the RouterDelegate
would be subscribing to the state changes via streams. For example:
- When the
RouterDelegate
is initialized, we subscribe to the authentication state changes via aStream
. - When the logout button is pressed on a screen, the
logout
method of the authentication repository is invoked (better through a use case class). - The repository class calls the methods in the data layer and the user is logged out.
- The authentication state change stream provides the logout event to its listeners by emitting the new state.
- The
RouterDelegate
receives the state change and updates the navigation stack accordingly.
We can also inject the use case classes to the RouterDelegate
and ViewModel classes of the widgets to interact with the repositories using dependency injection libraries. Although clean architecture is out of this topic’s scope, I would strongly recommend further reading if the reader is not familiar with the topic.
Conclusion
In this article, we discussed how to build a navigation stack in response to the app state changes caused by authentication state updates. 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_02.dart
file and selecting the Run 'main_002_02.dart'
.
In the next article, we will add the bootstrapping 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.