使用go_router异步重定向时Flutter组件不重建的问题排查
我之前已经通过Bloc Listener实现了基于Firebase认证状态的登录页到首页的导航,确认认证逻辑正常。现在按照Flutter官方推荐改用GoRouter实现,但遇到问题:当认证流产生新事件时,尽管AuthChangeNotifier已经触发了notifyListeners(),GoRouter的redirect方法仍不会被调用,组件也不会重建。
涉及的四个组件:
- go_router(导航器)
- Firebase(登录提供方)
- AuthChangeNotifier(参照官方示例实现)
- AuthNotifier(参照官方示例实现)
我清楚自己的实现和官方示例有两处差异:
- 在GoRouter的
redirect方法中直接使用Firebase客户端,而非通过AuthNotifier.of(context)获取状态 - AuthNotifier并非作为根节点加入组件树,而是在
MultipleBlocProviders和MultipleRepositoryProvider之后
相关代码
ROUTER:
GoRouter( redirect: (context, state) async { // 与示例不同:直接使用Firebase客户端而非AuthNotifier final currentUser = await RepositoryProvider.of<auth_repo.FirebaseAuthenticator>( context, listen: false) .currentUserStream() .first; // 此处能正确获取到用户 bool? isSignedIn = await currentUser?.isSignedIn(); final bool loggingIn = state.matchedLocation == '/login'; if (loggingIn || isSignedIn == null || isSignedIn == false) { return '/login'; } if (isSignedIn) { return '/home'; } return null; }, routes: <RouteBase>[ GoRoute( path: '/home', builder: (BuildContext context, GoRouterState state) { return const HomePage(); }, routes: <RouteBase>[ GoRoute( path: 'account', builder: (BuildContext context, GoRouterState state) { return AccountPage(); }, ), ], ), GoRoute( path: '/login', builder: (BuildContext context, GoRouterState state) { return const LoginPage(); }, ), ], );
AuthChangeNotifier
class AuthChangeNotifier extends ChangeNotifier { Stream<auth_repo.User> userStream; AuthChangeNotifier(this.userStream) { userStream.asBroadcastStream().listen((_) { notifyListeners(); // 此处会被调用 }); } }
AuthNotifier
class AuthNotifier extends InheritedNotifier<AuthChangeNotifier> { AuthNotifier({ Key? key, required auth_repo.AuthenticatorInterface authenticator, required Widget child, }) : super( key: key, notifier: AuthChangeNotifier(authenticator.currentUserStream()), child: child, ); static Stream<auth_repo.User> of(BuildContext context) { final AuthChangeNotifier? result = context.dependOnInheritedWidgetOfExactType<AuthNotifier>()!.notifier; if (result == null) { throw FlutterError( 'AuthNotifier.of() called with a context that does not contain an AuthNotifier.'); } return result.userStream; } }
AuthNotifier的使用
// 与示例不同:先实例化所有Bloc和Provider,再包裹AuthNotifier return _blocProvider( _repositoryProvider( AuthNotifier(authenticator: _authenticator, child: const MyApp()), ), );
问题原因与修复方案
核心问题
GoRouter的redirect方法要能响应认证状态变化,必须依赖AuthNotifier这个InheritedNotifier,或者通过refreshListenable绑定AuthChangeNotifier。你的当前实现中,redirect直接调用Firebase客户端获取流,但没有让GoRouter感知到这个流的状态变化,同时也没有依赖AuthNotifier,所以即使notifyListeners()被调用,GoRouter也不会重新执行redirect。
具体修复步骤
修改GoRouter的redirect逻辑,依赖AuthNotifier
在redirect中调用AuthNotifier.of(context),这个方法内部的context.dependOnInheritedWidgetOfExactType会让GoRouter的上下文绑定AuthNotifier,当AuthChangeNotifier触发通知时,GoRouter会自动重新执行redirect。同时调整重定向逻辑的合理性:redirect: (context, state) async { // 通过AuthNotifier获取用户流,建立依赖关系 final userStream = AuthNotifier.of(context); final currentUser = await userStream.first; bool? isSignedIn = await currentUser?.isSignedIn(); final bool loggingIn = state.matchedLocation == '/login'; // 未登录且不在登录页时,跳转到登录页 if (isSignedIn == null || isSignedIn == false) { return loggingIn ? null : '/login'; } // 已登录但在登录页时,跳转到首页 if (isSignedIn && loggingIn) { return '/home'; } // 其他情况不重定向 return null; },添加refreshListenable绑定(可选但更可靠)
结合官方示例的做法,给GoRouter传入refreshListenable参数,直接绑定AuthChangeNotifier,确保状态变化时GoRouter主动刷新:// 在创建GoRouter前,先获取AuthChangeNotifier实例 final authChangeNotifier = AuthChangeNotifier(_authenticator.currentUserStream()); GoRouter( refreshListenable: authChangeNotifier, redirect: (context, state) async { final currentUser = await authChangeNotifier.userStream.first; bool? isSignedIn = await currentUser?.isSignedIn(); final bool loggingIn = state.matchedLocation == '/login'; if (isSignedIn == null || isSignedIn == false) { return loggingIn ? null : '/login'; } if (isSignedIn && loggingIn) { return '/home'; } return null; }, // ... 路由配置 )这种方式不需要依赖InheritedNotifier的上下文,更直接地让GoRouter监听状态变化。
确认AuthNotifier的组件树位置
只要GoRouter所在的上下文能访问到AuthNotifier,当前的位置(在Bloc和RepositoryProvider之后)是没问题的。确保MyApp中的GoRouter是在AuthNotifier的子树内,即GoRouter的context可以调用AuthNotifier.of(context)获取到实例。
额外优化点
- 当前
AuthNotifier.of()返回的是Stream,你可以修改它返回AuthChangeNotifier实例,方便在redirect中直接获取状态,减少重复代码。 - 避免在redirect中重复监听流,使用
.first获取当前最新状态即可,因为refreshListenable或InheritedNotifier会触发redirect重新执行,每次都会获取最新状态。
内容的提问来源于stack exchange,提问作者Wiktoor

