Conquering Authentication States in Your Flutter App
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.dart
and 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 aRouteSettings
object with the name of the required route to trigger navigation. - If the user is authenticated (
authState
isAuthState.authenticated
), theredirect
method returnsnull
, 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:
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!