Conquering Authentication States in Your Flutter App

Asaf Varon
Israeli Tech Radar
Published in
4 min readJul 1, 2024

As a mobile architect in Tikal working with many clients, I understand firsthand the importance of a seamless user experience. This is especially true when it comes to multiple layers of authentication, where users navigate between login, verification (biometric), and finally, accessing the app. However, managing these different states can get tricky in a Flutter app.

In this post, we’ll explore a clean approach using an enum, an AuthManager, and AuthMiddleware to handle various authentication states and ensure users only see what they’re authorized for.

Using an Enum for Defined States

First, let’s define our possible authentication states. We can create an Enum to represent these states:

enum AuthState {
none,
loginRequired,
biometricRequired,
authenticated,
}

This Enum clearly outlines the different stages a user can be in:

  • None: The app is just starting, and no authentication check has been made.
  • LoginRequired: The user needs to log in (username/password, social login, etc.).
  • BiometricRequired: The user has logged in but needs additional verification (fingerprint, face ID).
  • Authenticated: The user is fully logged in and can access the app.

The Power of the AuthManager

Now, we need a class to manage these states and their transitions. Create the AuthManager:

class AuthManager {
final AuthRepository authRepository;
AuthManager({required this.authRepository});

// default for starting the app
AuthState authState = AuthState.none;
bool isBiometricRequired = true;

Future<AuthManager> init() async {
await _checkAuthState();
return this;
}

_checkAuthState() async {
final isLoggedIn = authRepository.isLoggedIn();
if (!isLoggedIn) {
authState = AuthState.loginRequired;
return;
}
if (isBiometricRequired) {
authState = AuthState.biometricRequired;
return;
}
authState = AuthState.authenticated;
}
}

The AuthManager holds the current authentication state (authState) starting at AuthState.none

The AuthManager will also handle login, logout, and biometric verification logic, updating the state accordingly.

Future<bool> login() async {
final isLoggedIn = await authRepository.loginMock();
await _checkAuthState();
return isLoggedIn;
}
Future<bool> logout() async {
final isLoggedOut = await authRepository.logoutMock();
isBiometricRequired = true;
await _checkAuthState();
return isLoggedOut;
}
Future<bool> biometricAccessRequest() async {
final successBiometric = await authRepository.biometricLogin();
if (successBiometric) {
isBiometricRequired = false;
await _checkAuthState();
return true;
}
return false;
}

Declare Your Routes and Pages

To keep it clean and easy to maintain as we scale and add more screens, I like to keep things simple and separate as much as possible.

Create a dart file routes.dartand inside it create an enum for your Routes, and name the pages you want to navigate.

enum Routes {
home,
login,
biometric,
settings,
}

extension RouteUtils on Routes {
String get path => switch (this) {
Routes.home => "/",
_ => "/$name",
};
}

Next, we want to create a file containing all the needed pages.
Because we are using GetX it is easy for us to create a list of GetPage and feed it to our Material App.

Side Note: Checkout GetX for further usage of the libraries capabilities and simplife your Flutter development: https://pub.dev/packages/get

final pages = [
GetPage(
name: Routes.home.path,
// This will be triggered everytime we want to get the home page
middlewares: [AuthMiddleware(locator.get<AuthManager>())],
page: () => HomePage(),
),
GetPage(
name: Routes.login.path,
page: () => LoginPage(),
),
GetPage(
name: Routes.biometric.path,
page: () => BiometricPage(),
),
];

Note that we have declared the AuthMiddleware only inside our home page. This ensures anyone trying to access the HomePage route (with name /home) will be redirected if they are not authenticated.

This approach leverages GetX features for a more concise and integrated solution for authentication flow management.

Side Note: locator.get<AuthManager>() is a GetIt usage as a dependency injection. checkout this link for further usage and examples: https://pub.dev/packages/get_it

Authentication Middleware — Your Gatekeeper

Instead of a custom middleware class, we can leverage the power of GetX’s built-in GetMiddleware. This offers a cleaner approach with the redirect function:

class AuthMiddleware extends GetMiddleware {
final AuthManager _authManager;

AuthMiddleware(this._authManager);
@override
RouteSettings? redirect(String? route) {
debugPrint("authState: ${_authManager.authState}");
switch (_authManager.authState) {
case AuthState.none:
case AuthState.loginRequired:
return RouteSettings(name: Routes.login.path);
case AuthState.biometricRequired:
return RouteSettings(name: Routes.biometric.path);
// returning null when we don't need to redirect
case AuthState.authenticated:
return null;
}
}
}

This AuthMiddleware class extends GetMiddleware. The crucial method is redirect, And the AuthManager as a dependency to check the current authentication state.

  • If the state is not AuthState.authenticated, the middleware returns a RouteSettings object with the name of the required route to trigger navigation.
  • If the user is authenticated (authState is AuthState.authenticated), the redirect method returns null, allowing access to the intended route.

Seal The Deal

Now, all that is left to do is to add your getPages, and define our initialRoute as your Home to our GetMaterialApp.

GetMaterialApp(
initialRoute: Routes.home.path,
getPages: pages,
);

This approach offers a clean separation of concerns and makes managing authentication flow throughout your Flutter app easy. You can extend this further by adding functionalities like handling expired sessions or refreshing tokens based on your specific authentication needs.

For the full example using MultiModule application with Repository Pattern and handling multiple authentication states, navigations, and state management check this repo:

https://github.com/asafvaron90/flutter_workshop

Remember, a well-managed authentication flow is key to a smooth user experience, especially for the onboarding and returning users. So, take control of your authentication states and keep your Flutter app secure and user-friendly!

--

--