Skip to main content
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

Route guards with metadata

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

1

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]);
      }
    }
  }
}
2

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.
3

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)),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
4

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();
Preserve the initial route and navigate after authentication:
if (loadInitialRoute) {
  loadInitialRoute = false;
  String initialRoute =
      NavigationManager.instance.routeInformationParser.initialRoute;
  NavigationManager.instance.set([initialRoute]);
}

Route metadata for guards

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

Build docs developers (and LLMs) love