// Version: 1.0.2 | Created: 2026-04-01 // All route definitions in one place. // GoRouter is created ONCE (keepAlive) and re-evaluates redirects via // refreshListenable — avoids the GoRouter recreation bug from ref.watch. import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../core/auth/auth_notifier.dart'; import '../core/auth/auth_state.dart'; import '../features/auth/presentation/login_screen.dart'; import '../features/calls/presentation/call_screen.dart'; import '../features/chat/presentation/chat_screen.dart'; import '../features/profile/presentation/profile_screen.dart'; import '../features/rooms/presentation/rooms_screen.dart'; import '../features/spaces/presentation/spaces_screen.dart'; part 'router.g.dart'; /// Route path constants — use these instead of raw strings. abstract final class AppRoutes { static const String root = '/'; static const String login = '/login'; static const String rooms = '/rooms'; static const String chat = '/rooms/:roomId'; static const String call = '/calls/:roomId'; static const String profile = '/profile'; static const String spaces = '/spaces'; } /// ChangeNotifier that GoRouter listens to for redirect re-evaluation. /// Notified by ref.listen whenever authProvider changes. class _AuthRefreshNotifier extends ChangeNotifier { void notify() => notifyListeners(); } @Riverpod(keepAlive: true) GoRouter router(Ref ref) { // Create once — notifier triggers re-redirect without recreating the router. final notifier = _AuthRefreshNotifier(); ref.listen(authProvider, (_, __) => notifier.notify()); ref.onDispose(notifier.dispose); return GoRouter( initialLocation: AppRoutes.root, refreshListenable: notifier, debugLogDiagnostics: false, redirect: (BuildContext context, GoRouterState state) { // Read (not watch) so redirect does not itself trigger provider changes. final authState = ref.read(authProvider); final isLoggedIn = authState is AuthAuthenticated; final isOnLogin = state.matchedLocation == AppRoutes.login; // While restoring session, stay on splash. if (authState is AuthInitial || authState is AuthLoading) return null; // Unauthenticated users must go to login. if (!isLoggedIn && !isOnLogin) return AppRoutes.login; // Authenticated users should not see the login screen. if (isLoggedIn && isOnLogin) return AppRoutes.rooms; return null; }, routes: [ GoRoute( path: AppRoutes.root, builder: (context, state) => const _SplashRedirectPage(), ), GoRoute( path: AppRoutes.login, builder: (context, state) => const LoginScreen(), ), GoRoute( path: AppRoutes.rooms, builder: (context, state) => const RoomsScreen(), ), GoRoute( path: AppRoutes.chat, builder: (context, state) { // safe: path param is guaranteed by route definition final roomId = state.pathParameters['roomId']!; return ChatScreen(roomId: roomId); }, ), GoRoute( path: AppRoutes.call, builder: (context, state) { // safe: path param is guaranteed by route definition final roomId = state.pathParameters['roomId']!; return CallScreen(roomId: roomId); }, ), GoRoute( path: AppRoutes.profile, builder: (context, state) => const ProfileScreen(), ), GoRoute( path: AppRoutes.spaces, builder: (context, state) => const SpacesScreen(), ), ], ); } /// Invisible page shown at `/` while GoRouter's redirect evaluates auth state. class _SplashRedirectPage extends StatelessWidget { const _SplashRedirectPage(); @override Widget build(BuildContext context) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } }