This example demonstrates a complete authentication system with login/signup flows, route guards, and Firebase Auth integration used by 10,000+ users in production.
Overview
The authentication flow includes:
Complete login/signup/reset password screens
Route guards for authenticated pages
Automatic navigation on auth state changes
Deep link handling during authentication
Firebase Auth integration
Loading and initialization state management
Key concepts
Use metadata to mark routes that require authentication:
List < NavigationData > get routes => [
NavigationData (
label : HomePage .name,
url : '/' ,
builder : (context, routeData, globalData) => const HomePage (),
metadata : { 'auth' : true }), // Requires authentication
NavigationData (
label : LoginForm .name,
url : '/login' ,
group : 'auth' ,
builder : (context, routeData, globalData) =>
AuthPage (type : AuthPageType .login),
metadata : { 'type' : 'auth' }), // Auth pages
NavigationData (
label : SignUpForm .name,
url : '/signup' ,
group : 'auth' ,
builder : (context, routeData, globalData) =>
AuthPage (type : AuthPageType .signup),
metadata : { 'type' : 'auth' }),
];
The group: 'auth' parameter ensures login and signup pages share the same widget instance, preserving state during transitions.
Route filtering callback
Implement setMainRoutes to filter routes based on authentication state:
NavigationManager .instance.setMainRoutes = (routes) => setMainRoutes (routes);
List < DefaultRoute > setMainRoutes ( List < DefaultRoute > routes) {
List < DefaultRoute > routesHolder = routes;
// Remove authenticated routes if user is not logged in
if ( AuthService .instance.isAuthenticated.value == false ) {
routesHolder. removeWhere ((element) => element.metadata ? [ 'auth' ] == true );
if (routesHolder.isEmpty) {
routesHolder. add ( DefaultRoute (label : SignUpForm .name, path : '/signup' ));
NavigationManager .instance. resumeNavigation ();
}
}
// Remove auth pages if user is logged in
if ( AuthService .instance.isAuthenticated.value) {
routesHolder. removeWhere ((element) => element.metadata ? [ 'type' ] == 'auth' );
if (routesHolder.isEmpty) {
routesHolder. add ( NavigationUtils . buildDefaultRouteFromName (
navigation_routes.routes, '/' ));
}
}
return routesHolder;
}
The setMainRoutes callback is invoked before every navigation event, allowing you to dynamically control which routes are accessible.
Implementation
Set up Firebase listeners
Listen to Firebase Auth state changes in your app’s initState: class AppState extends State < App > {
late StreamSubscription < String ?> firebaseAuthUserListener;
@override
void initState () {
super . initState ();
init ();
}
Future < void > init () async {
// Attach navigation route guard callback
NavigationManager .instance.setMainRoutes = (routes) => setMainRoutes (routes);
// Listen to Firebase Auth state changes
firebaseAuthUserListener = AuthService .instance.firebaseAuthUserStream
. listen ((uid) =>
uid != null ? onUserAuthenticated (uid) : onUserUnauthenticated ());
// Set initial route based on current auth state
if ( AuthService .instance.isAuthenticated.value) {
onUserAuthenticated ( UserManager .instance.user.value.id);
} else {
if ( UserManager .instance.user.value.empty == false ) {
// Wait for FirebaseAuth to initialize
NavigationManager .instance. pauseNavigation ();
} else {
NavigationManager .instance. set ([ SignUpForm .name]);
}
}
}
}
Handle authentication events
Navigate appropriately when auth state changes: Future < void > onUserAuthenticated ( String uid) async {
// Load the initial route on first authentication
if (loadInitialRoute) {
loadInitialRoute = false ;
initialized = true ;
String initialRoute =
NavigationManager .instance.routeInformationParser.initialRoute;
NavigationManager .instance. set ([initialRoute]);
} else {
NavigationManager .instance. set ([ HomePage .name]);
}
NavigationManager .instance. resumeNavigation ();
}
Future < void > onUserUnauthenticated () async {
// Redirect to auth screen if on an authenticated page
if ( NavigationManager .instance.currentRoute ? .metadata ? [ 'auth' ] == true ) {
NavigationManager .instance. set ([ SignUpForm .name]);
}
NavigationManager .instance. resumeNavigation ();
SignoutHelper . signOut ();
}
pauseNavigation() prevents navigation during async initialization. Always call resumeNavigation() once initialization completes.
Create auth pages
Implement login, signup, and reset password pages: class HomePage extends StatefulWidget {
static const String name = 'home' ;
const HomePage ({ super .key});
@override
State < HomePage > createState () => _HomePageState ();
}
class _HomePageState extends State < HomePage > {
@override
Widget build ( BuildContext context) {
return const PageWrapper (
child : SizedBox (
width : double .infinity,
height : double .infinity,
child : Center (
child : Column (
mainAxisAlignment : MainAxisAlignment .center,
children : [
Text ( 'Home Page' ),
MaterialButton (
onPressed : SignoutHelper .signOut,
color : Colors .blue,
child : Text ( 'Logout' , style : TextStyle (color : Colors .white)),
),
],
),
),
),
);
}
}
Configure MaterialApp
Set up MaterialApp.router with system UI styling: @override
Widget build ( BuildContext context) {
SystemChrome . setSystemUIOverlayStyle ( const SystemUiOverlayStyle (
statusBarColor : Colors .transparent,
systemNavigationBarContrastEnforced : true ,
systemNavigationBarColor : Colors .transparent,
statusBarIconBrightness : Brightness .dark,
statusBarBrightness : Brightness .dark));
return MaterialApp . router (
title : 'Example Auth' ,
routerDelegate : NavigationManager .instance.routerDelegate,
routeInformationParser : NavigationManager .instance.routeInformationParser,
);
}
Complete example structure
Here’s the full authentication flow implementation:
import 'dart:async' ;
import 'package:flutter/foundation.dart' ;
import 'package:flutter/material.dart' ;
import 'package:navigation_utils/navigation_utils.dart' ;
class App extends StatefulWidget {
static const String name = 'app' ;
const App ({ super .key});
@override
State < App > createState () => AppState ();
}
class AppState extends State < App > {
bool initialized = false ;
bool loadInitialRoute = true ;
late StreamSubscription < String ?> firebaseAuthUserListener;
@override
void initState () {
super . initState ();
init ();
}
Future < void > init () async {
// Attach navigation callback that hooks into the app state
NavigationManager .instance.setMainRoutes = (routes) => setMainRoutes (routes);
firebaseAuthUserListener = AuthService .instance.firebaseAuthUserStream
. listen ((uid) =>
uid != null ? onUserAuthenticated (uid) : onUserUnauthenticated ());
// Set initialization page
if ( AuthService .instance.isAuthenticated.value) {
onUserAuthenticated ( UserManager .instance.user.value.id);
} else {
if ( UserManager .instance.user.value.empty == false ) {
// Wait for FirebaseAuth to initialize
NavigationManager .instance. pauseNavigation ();
} else {
NavigationManager .instance. set ([ SignUpForm .name]);
}
}
}
@override
void dispose () {
firebaseAuthUserListener. cancel ();
super . dispose ();
}
Future < void > onUserAuthenticated ( String uid) async {
// Attempt to load the initial route URI
if (loadInitialRoute) {
loadInitialRoute = false ;
initialized = true ;
String initialRoute =
NavigationManager .instance.routeInformationParser.initialRoute;
NavigationManager .instance. set ([initialRoute]);
} else {
NavigationManager .instance. set ([ HomePage .name]);
}
NavigationManager .instance. resumeNavigation ();
}
Future < void > onUserUnauthenticated () async {
// Automatically navigate to auth screen when user is logged out
if ( NavigationManager .instance.currentRoute ? .metadata ? [ 'auth' ] == true ) {
NavigationManager .instance. set ([ SignUpForm .name]);
}
NavigationManager .instance. resumeNavigation ();
SignoutHelper . signOut ();
}
List < DefaultRoute > setMainRoutes ( List < DefaultRoute > routes) {
List < DefaultRoute > routesHolder = routes;
// Authenticated route guard
if ( AuthService .instance.isAuthenticated.value == false ) {
routesHolder. removeWhere ((element) => element.metadata ? [ 'auth' ] == true );
if (routesHolder.isEmpty) {
routesHolder. add ( DefaultRoute (label : SignUpForm .name, path : '/signup' ));
NavigationManager .instance. resumeNavigation ();
}
}
// Remove login and signup page guard
if ( AuthService .instance.isAuthenticated.value) {
routesHolder. removeWhere ((element) => element.metadata ? [ 'type' ] == 'auth' );
if (routesHolder.isEmpty) {
routesHolder. add ( NavigationUtils . buildDefaultRouteFromName (
navigation_routes.routes, '/' ));
}
}
return routesHolder;
}
@override
Widget build ( BuildContext context) {
return MaterialApp . router (
title : 'Example Auth' ,
routerDelegate : NavigationManager .instance.routerDelegate,
routeInformationParser : NavigationManager .instance.routeInformationParser,
);
}
}
Key patterns
Pause and resume navigation
Use these methods during async initialization:
// Pause during async operations
NavigationManager .instance. pauseNavigation ();
// Resume when ready
NavigationManager .instance. resumeNavigation ();
Handle deep links during auth
Preserve the initial route and navigate after authentication:
if (loadInitialRoute) {
loadInitialRoute = false ;
String initialRoute =
NavigationManager .instance.routeInformationParser.initialRoute;
NavigationManager .instance. set ([initialRoute]);
}
Use metadata to categorize routes:
metadata : { 'auth' : true } // Requires authentication
metadata : { 'type' : 'auth' } // Auth pages (login/signup)
metadata : { 'role' : 'admin' } // Custom role-based guards
Testing credentials
For the example_auth demo:
Email: test@testuser.com
Password: 12345678
Next steps
Nested tabs Build complex nested navigation with tabs
Custom transitions Add custom page transition animations