基于go_router与flutter_bloc实现认证/未认证流程的响应式路由跳转
需求说明
- 应用启动时根据认证状态跳转:已认证→HomeScreen,未认证→LoginScreen,状态未知→SplashScreen
- 已认证状态下,支持在HomeScreen、TestScreen1、TestScreen2之间自由导航
- 任何时刻认证状态变化时,自动重定向到对应页面
当前遇到的问题
问题1:认证流程内导航报错
从HomeScreen跳转TestScreen1/TestScreen2时,触发断言错误:
Tried to listen to a value exposed with provider, from outside of the widget tree.
This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passinglisten: false.To fix, write:
Provider.of<$T>(context, listen: false);It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.The context used was: $context
问题2:Web端手动修改URL报错
在HomeScreen(路径/auth/home_screen)时手动修改URL为/login_screen,抛出异常:
========= Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for GoRouteInformationProvider:
Tried to listen to a value exposed with provider, from outside of the widget tree.This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passinglisten: false.
注:不想用context.read替换context.watch,因为需要监听Bloc状态变化
现有实现代码
AppRouter代码
class AppRouter { GoRouter get router => _goRouter; late final GoRouter _goRouter = GoRouter( routes: <GoRoute>[ GoRoute( path: '/', name: 'SPLASH', builder: (context, state) => const SplashScreen(), ), GoRoute( path: '/auth', name: 'AUTH', builder: (context, state) => HomeScreen(), routes: <GoRoute>[ GoRoute( path: 'home_screen', name: 'HOME', builder: (context, state) => HomeScreen(), ), GoRoute( path: 'test_screen_1', name: 'TEST_SCREEN_1', builder: (context, state) => TESTSCREEN1(), ), GoRoute( path: 'test_screen_2', name: 'TEST_SCREEN_2', builder: (context, state) => TESTSCREEN2(), ), ], ), GoRoute( path: '/login_screen', name: 'LOGIN', builder: (context, state) => LoginScreen(), ), ], redirect: (context, state) { final authState = context.watch<AuthBloc>().state; final loginLocation = state.namedLocation('LOGIN'); final authFlow = state.namedLocation('AUTH'); final splashLocation = state.namedLocation('SPLASH'); if (authState is AuthStateAuthenticated) { return authFlow; } else if (authState is AuthStateUnAuthenticated) { return loginLocation; } else { return splashLocation; } }, ); }
App代码
class App extends StatelessWidget { AppRouter appRouter = AppRouter(); @override Widget build(BuildContext context) { return RepositoryProvider<AuthService>( create: (context) => AuthService(), child: MultiBlocProvider( providers: [ BlocProvider<AuthBloc>(create: (context) => AuthBloc(authService: RepositoryProvider.of<AuthService>(context))), ], child: MaterialApp.router( routerDelegate: appRouter.router.routerDelegate, routeInformationParser: appRouter.router.routeInformationParser, routeInformationProvider: appRouter.router.routeInformationProvider, ), ), ); } }
解决方案
核心问题分析
GoRouter的redirect方法不在Widget树上下文内执行,直接用context.watch会触发"outside of the widget tree"错误。需要通过Bloc的状态流监听变化,而非依赖Widget树上下文。
步骤1:重构AppRouter,绑定Bloc流作为刷新源
class AppRouter { final AuthBloc authBloc; AppRouter(this.authBloc); late final GoRouter _goRouter = GoRouter( routes: <GoRoute>[ GoRoute( path: '/', name: 'SPLASH', builder: (context, state) => const SplashScreen(), ), GoRoute( path: '/auth', name: 'AUTH', builder: (context, state) => HomeScreen(), routes: <GoRoute>[ GoRoute( path: 'home_screen', name: 'HOME', builder: (context, state) => HomeScreen(), ), GoRoute( path: 'test_screen_1', name: 'TEST_SCREEN_1', builder: (context, state) => TESTSCREEN1(), ), GoRoute( path: 'test_screen_2', name: 'TEST_SCREEN_2', builder: (context, state) => TESTSCREEN2(), ), ], ), GoRoute( path: '/login_screen', name: 'LOGIN', builder: (context, state) => LoginScreen(), ), ], redirect: (context, state) { // 用read获取当前状态,无需监听(状态变化由refreshListenable触发) final authState = context.read<AuthBloc>().state; final loginLocation = state.namedLocation('LOGIN'); final authHomeLocation = state.namedLocation('HOME'); final splashLocation = state.namedLocation('SPLASH'); // 已认证状态:不在认证流程内则跳Home,否则留当前页 if (authState is AuthStateAuthenticated) { return state.fullPath!.startsWith('/auth') ? null : authHomeLocation; } // 未认证状态:不在登录页则跳转,否则留当前页 else if (authState is AuthStateUnAuthenticated) { return state.fullPath == '/login_screen' ? null : loginLocation; } // 状态未知:不在闪屏页则跳转,否则留当前页 else { return state.fullPath == '/' ? null : splashLocation; } }, // 绑定Bloc流,状态变化时自动触发重定向 refreshListenable: GoRouterRefreshStream(authBloc.stream), ); GoRouter get router => _goRouter; } // 自定义Listenable,将Bloc流转换为GoRouter可监听的源 class GoRouterRefreshStream extends ChangeNotifier { GoRouterRefreshStream(Stream<dynamic> stream) { _subscription = stream.listen((_) => notifyListeners()); } late final StreamSubscription<dynamic> _subscription; @override void dispose() { _subscription.cancel(); super.dispose(); } }
步骤2:修改App类,传递AuthBloc实例
在BlocProvider创建后再初始化AppRouter,确保能获取到Bloc实例:
class App extends StatelessWidget { @override Widget build(BuildContext context) { return RepositoryProvider<AuthService>( create: (context) => AuthService(), child: BlocProvider<AuthBloc>( create: (context) => AuthBloc(authService: RepositoryProvider.of<AuthService>(context)), child: Builder( builder: (context) { final authBloc = BlocProvider.of<AuthBloc>(context); final appRouter = AppRouter(authBloc); return MaterialApp.router( routerDelegate: appRouter.router.routerDelegate, routeInformationParser: appRouter.router.routeInformationParser, routeInformationProvider: appRouter.router.routeInformationProvider, ); }, ), ), ); } }
步骤3:认证流程内导航修复
现在在HomeScreen等页面内直接使用以下代码导航即可,不会再触发错误:
GoRouter.of(context).pushNamed('TEST_SCREEN_1');
关键说明
- 通过
GoRouterRefreshStream将AuthBloc的状态流转换为GoRouter的刷新源,状态变化时自动触发重定向 - redirect内用
context.read获取当前状态,避免在非Widget树上下文内监听状态 - 重定向逻辑增加当前路径判断,避免无限重定向循环
内容的提问来源于stack exchange,提问作者Taha Aslam

