fix: prevent GoRouter recreation on auth state change

routerProvider was watching authProvider and returning a new GoRouter
on every status transition (unknown → authenticated). This caused
MaterialApp.router to rebuild the entire navigation tree, which
triggered all data providers to start loading before auth was confirmed.

Switch to refreshListenable pattern: GoRouter is created once,
_RouterNotifier fires notifyListeners() when auth changes, and GoRouter
re-runs redirect using ref.read(authProvider). Add /loading splash route
shown during AuthStatus.unknown so no authenticated screen (and no API
call) is initiated until the stored-token check completes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-02-22 17:42:29 +02:00
parent 0f2d86efdb
commit ea11a423e0

View File

@@ -22,21 +22,38 @@ import '../../features/products/product_provider.dart';
import '../../shared/models/recipe.dart'; import '../../shared/models/recipe.dart';
import '../../shared/models/saved_recipe.dart'; import '../../shared/models/saved_recipe.dart';
// Notifies GoRouter when auth state changes without recreating the router.
class _RouterNotifier extends ChangeNotifier {
_RouterNotifier(Ref ref) {
ref.listen<AuthState>(authProvider, (_, __) => notifyListeners());
}
}
final routerProvider = Provider<GoRouter>((ref) { final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authProvider); final notifier = _RouterNotifier(ref);
ref.onDispose(notifier.dispose);
return GoRouter( return GoRouter(
initialLocation: '/home', initialLocation: '/loading',
refreshListenable: notifier,
redirect: (context, state) { redirect: (context, state) {
// Use ref.read — this runs inside GoRouter, not inside a Riverpod build.
final authState = ref.read(authProvider);
final isLoggedIn = authState.status == AuthStatus.authenticated; final isLoggedIn = authState.status == AuthStatus.authenticated;
final isAuthRoute = state.matchedLocation.startsWith('/auth'); final isAuthRoute = state.matchedLocation.startsWith('/auth');
if (authState.status == AuthStatus.unknown) return null; // Show splash until the stored-token check completes.
if (authState.status == AuthStatus.unknown) return '/loading';
if (!isLoggedIn && !isAuthRoute) return '/auth/login'; if (!isLoggedIn && !isAuthRoute) return '/auth/login';
if (isLoggedIn && isAuthRoute) return '/home'; if (isLoggedIn && isAuthRoute) return '/home';
return null; return null;
}, },
routes: [ routes: [
// Splash shown while auth status is unknown.
GoRoute(
path: '/loading',
builder: (_, __) => const _SplashScreen(),
),
GoRoute( GoRoute(
path: '/auth/login', path: '/auth/login',
builder: (_, __) => const LoginScreen(), builder: (_, __) => const LoginScreen(),
@@ -118,6 +135,15 @@ final routerProvider = Provider<GoRouter>((ref) {
); );
}); });
class _SplashScreen extends StatelessWidget {
const _SplashScreen();
@override
Widget build(BuildContext context) => const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
class _InvalidRoute extends StatelessWidget { class _InvalidRoute extends StatelessWidget {
const _InvalidRoute(); const _InvalidRoute();