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:
-
Is user authenticated?
- No → Show Auth Screen (Login/Signup)
- Yes → Check onboarding status
-
Has user completed onboarding?
- No → Show Onboarding Screen
- Yes → Show Home Screen
Architecture Diagram
textApp 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)
dartimport '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
dartclass 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
dartclass 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
SharedPreferencesdart// 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