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

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:

  • Inside the setter method of the loggedIn state notifyListeners() method is called and the Router widget is notified.
  • The Router widget asks the RouterDelegate to build a new Navigator 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 the Navigator 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 called
  • logingIn state inside the AuthViewModel is set to true and the LoginScreen is notified about the state change.
  • LoginScreen rebuilds and shows the InProgressMessage widget which is a text centered in the screen.
  • AuthRepository.login method is called and the result is returned to the AuthViewModel from the AuthRepository with a delay.
  • loggingIn state is set to false and the LoginScreen is notified about the state change.
  • If the result is true , the login process is successful. The onLogin callback which is defined in the RouterDelegate and passed down to the LoginScreen widget is invoked.

Now let’s see what happens in the RouterDelegate when the onLogin callback is invoked:

  • TheloggedIn state is set true
  • Inside the setter method of loggedIn , the Router widget is notified by calling the notifyListeners() method.
  • Router widget calls the build method of the RouterDelegate .
  • RouterDelegate constructs and returns a new Navigator widget to the Router 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 called
  • loggingOut state inside the AuthViewModel is set to true and the LogoutFab 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 the AuthRepository with a delay.
  • loggingOut state is set to false and the LogoutFab widget is notified about the state change.
  • onLogout callback which is defined in the RouterDelegate and passed down to the LogoutFab widget is invoked.

Now let’s see what happens in the RouterDelegate when the onLogout callback is invoked:

  • TheloggedIn state is set to false .
  • 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 , the Router widget is notified by calling the notifyListeners() method.
  • Router widget calls the build method of the RouterDelegate .
  • RouterDelegate constructs and returns a new Navigator widget to the Router 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 a Stream.
  • When the logout button is pressed on a screen, thelogout 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.

Flutter and Android Mobile App Developer