Question #300MediumNavigation & Routing

Navigation architech for this 3 screens in correct order -> auth , onboarding , home screen ?

#navigation#authentication#onboarding#routing#architecture

Answer

Overview

Implementing proper navigation architecture for authentication, onboarding, and home screens is a common requirement in mobile apps. The correct flow ensures users see the right screen based on their authentication status and whether they've completed onboarding.

Navigation Flow Logic

The typical decision tree:

  1. Is user authenticated?

    • No → Show Auth Screen (Login/Signup)
    • Yes → Check onboarding status
  2. Has user completed onboarding?

    • No → Show Onboarding Screen
    • Yes → Show Home Screen

Architecture Diagram

text
App Launch
    |
    v
Is Authenticated?
    |
    +-- No --> Auth Screen
    |              |
    |              v
    |         Login Success
    |              |
    +-- Yes -------+
                   |
                   v
          Has Seen Onboarding?
                   |
                   +-- No --> Onboarding Screen
                   |              |
                   |              v
                   |         Complete Onboarding
                   |              |
                   +-- Yes -------+
                                  |
                                  v
                             Home Screen

Implementation Options

Option 1: Using go_router (Recommended)

dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';

// Auth service to manage authentication state
class AuthService extends ChangeNotifier {
  bool _isAuthenticated = false;
  bool _hasCompletedOnboarding = false;

  bool get isAuthenticated => _isAuthenticated;
  bool get hasCompletedOnboarding => _hasCompletedOnboarding;

  Future<void> init() async {
    final prefs = await SharedPreferences.getInstance();
    _isAuthenticated = prefs.getBool('isAuthenticated') ?? false;
    _hasCompletedOnboarding = prefs.getBool('hasCompletedOnboarding') ?? false;
    notifyListeners();
  }

  Future<void> login() async {
    _isAuthenticated = true;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isAuthenticated', true);
    notifyListeners();
  }

  Future<void> completeOnboarding() async {
    _hasCompletedOnboarding = true;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('hasCompletedOnboarding', true);
    notifyListeners();
  }

  Future<void> logout() async {
    _isAuthenticated = false;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isAuthenticated', false);
    notifyListeners();
  }
}

// Router configuration
class AppRouter {
  final AuthService authService;

  AppRouter(this.authService);

  late final GoRouter router = GoRouter(
    refreshListenable: authService,
    redirect: (context, state) {
      final isAuthenticated = authService.isAuthenticated;
      final hasCompletedOnboarding = authService.hasCompletedOnboarding;
      
      final isGoingToAuth = state.matchedLocation == '/auth';
      final isGoingToOnboarding = state.matchedLocation == '/onboarding';
      final isGoingToHome = state.matchedLocation == '/';

      // Not authenticated - redirect to auth
      if (!isAuthenticated) {
        return isGoingToAuth ? null : '/auth';
      }

      // Authenticated but hasn't completed onboarding
      if (!hasCompletedOnboarding) {
        return isGoingToOnboarding ? null : '/onboarding';
      }

      // Authenticated and onboarded - prevent going back to auth/onboarding
      if (isGoingToAuth || isGoingToOnboarding) {
        return '/';
      }

      return null; // No redirect needed
    },
    routes: [
      GoRoute(
        path: '/auth',
        builder: (context, state) => AuthScreen(),
      ),
      GoRoute(
        path: '/onboarding',
        builder: (context, state) => OnboardingScreen(),
      ),
      GoRoute(
        path: '/',
        builder: (context, state) => HomeScreen(),
      ),
    ],
  );
}

// Main app setup
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final authService = AuthService();
  await authService.init();
  
  runApp(MyApp(authService: authService));
}

class MyApp extends StatelessWidget {
  final AuthService authService;

  const MyApp({required this.authService});

  
  Widget build(BuildContext context) {
    final appRouter = AppRouter(authService);
    
    return MaterialApp.router(
      routerConfig: appRouter.router,
      title: 'Navigation Demo',
    );
  }
}

Screen Implementations

dart
// Auth Screen
class AuthScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            final authService = context.read<AuthService>();
            await authService.login();
            // go_router will automatically redirect to onboarding or home
          },
          child: Text('Login'),
        ),
      ),
    );
  }
}

// Onboarding Screen
class OnboardingScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Welcome')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Onboarding Content Here'),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                final authService = context.read<AuthService>();
                await authService.completeOnboarding();
                // go_router will automatically redirect to home
              },
              child: Text('Get Started'),
            ),
          ],
        ),
      ),
    );
  }
}

// Home Screen
class HomeScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        actions: [
          IconButton(
            icon: Icon(Icons.logout),
            onPressed: () async {
              final authService = context.read<AuthService>();
              await authService.logout();
              // go_router will automatically redirect to auth
            },
          ),
        ],
      ),
      body: Center(
        child: Text('Welcome Home!'),
      ),
    );
  }
}

Option 2: Using Navigator 2.0

dart
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
  
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  final AuthService authService;

  AppRouterDelegate(this.authService) {
    authService.addListener(notifyListeners);
  }

  
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        // Determine which screen to show
        if (!authService.isAuthenticated)
          MaterialPage(child: AuthScreen())
        else if (!authService.hasCompletedOnboarding)
          MaterialPage(child: OnboardingScreen())
        else
          MaterialPage(child: HomeScreen()),
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }
        return true;
      },
    );
  }

  
  Future<void> setNewRoutePath(AppRoutePath path) async {
    // Handle external route changes
  }
}

Option 3: Using Simple Navigator with FutureBuilder

dart
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FutureBuilder<Map<String, bool>>(
        future: _checkAppState(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return SplashScreen();
          }

          final isAuthenticated = snapshot.data?['isAuthenticated'] ?? false;
          final hasSeenOnboarding = snapshot.data?['hasSeenOnboarding'] ?? false;

          if (!isAuthenticated) {
            return AuthScreen();
          } else if (!hasSeenOnboarding) {
            return OnboardingScreen();
          } else {
            return HomeScreen();
          }
        },
      ),
    );
  }

  Future<Map<String, bool>> _checkAppState() async {
    final prefs = await SharedPreferences.getInstance();
    return {
      'isAuthenticated': prefs.getBool('isAuthenticated') ?? false,
      'hasSeenOnboarding': prefs.getBool('hasSeenOnboarding') ?? false,
    };
  }
}

State Persistence

Use

text
SharedPreferences
to persist navigation state:

dart
// Save states
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isAuthenticated', true);
await prefs.setBool('hasCompletedOnboarding', true);

// Read states
final isAuthenticated = prefs.getBool('isAuthenticated') ?? false;
final hasCompletedOnboarding = prefs.getBool('hasCompletedOnboarding') ?? false;

Best Practices

  • Use go_router for production apps with complex navigation
  • Persist state in SharedPreferences or secure storage
  • Show splash screen while checking authentication state
  • Use streams for reactive authentication state changes
  • Prevent back navigation from home to auth/onboarding using
    text
    canPop: false
  • Handle edge cases like network errors during auth check
  • Test all navigation paths thoroughly
  • Clear sensitive data on logout

Common Mistakes to Avoid

  • Allowing users to navigate back to auth screen after login
  • Not persisting onboarding completion state
  • Showing onboarding every time app restarts
  • Not handling authentication state changes reactively
  • Forgetting to clear auth state on logout

Official Documentation