在Flutter中结合StreamBuilder与GoRouter持久化用户状态的问询
用GoRouter结合StreamBuilder实现Flutter用户认证状态持久化问题
问题概述
原本通过Flutter默认导航+StreamBuilder实现用户认证状态持久化,现在计划替换为GoRouter,想明确两个问题:
- 是否可以结合
StreamBuilder与GoRouter实现该功能? - 结合时在GoRouter代码中的具体使用位置?
同时当前的RouteNotifier类存在User类型无isEmpty属性的错误,需要修复。
原有默认导航实现代码
home: StreamBuilder( stream: AuthMethods().authChanges, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator(), ); } if (snapshot.hasData) { return const TabsScreen(); } return const AuthScreen(); }), ); }
当前GoRouter初始代码
GoRouter routeConstruct() { return GoRouter( routes: <RouteBase>[ GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) { return const PublicScreen(); }, routes: <RouteBase>[ GoRoute( path: Events_Screen, builder: (BuildContext context, GoRouterState state) { return const EventsScreen(); }, ), GoRoute( path: "$EventDetail_Screen/:eventId", builder: (BuildContext context, GoRouterState state) { return EventDetailScreen(eventId: state.params['eventId']!); }, ), ... ]), ], ); }
AuthMethods类代码
class AuthMethods { final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance; Stream<User?> get authChanges => _auth.authStateChanges(); // --> 使用了!运算符 User get user => _auth.currentUser!; Future<bool> signInWithGoogle(BuildContext context) async { bool res = false; try { final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); final GoogleSignInAuthentication? googleAuth = await googleUser?.authentication; final credential = GoogleAuthProvider.credential( accessToken: googleAuth?.accessToken, idToken: googleAuth?.idToken); UserCredential userCredential = await _auth.signInWithCredential(credential); User? user = userCredential.user; if (user != null) { if (userCredential.additionalUserInfo!.isNewUser) { await _firestore.collection('users').doc(user.uid).set({ 'username': user.displayName, 'uid': user.uid, 'profilePhoto': user.photoURL, }); } res = true; } } on FirebaseAuthException catch (e) { showSnackBar(context, e.message!); res = false; } return res; } }
RouteNotifier类代码(存在错误)
class RouterNotifier extends ChangeNotifier { RouterNotifier(this._authMethods) { _authMethods.authChanges.listen((event) { notifyListeners(); }); } final AuthMethods _authMethods; String? redirect(BuildContext context, GoRouterState state) { final onLoginPage = state.location == '/$AuthScreen'; final onHomePage = state.location == '/$Tabs_Screen'; // --> 错误:类型'User'没有定义'isEmpty' getter // --> User get user => _auth.currentUser!; 使用了!运算符 if (_authMethods.user.isEmpty && onHomePage) { return '/$AuthScreen'; } // --> 错误:类型'User'没有定义'isNotEmpty' getter if (_authMethods.currentUser.isNotEmpty && onLoginPage) { return '/$Tabs_Screen'; } return null; } }
集成后的router.dart代码
GoRouter routeConstruct() { return GoRouter( refreshListenable: router, routes: <RouteBase>[ GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) { return const PublicScreen(); }, routes: <RouteBase>[ GoRoute( path: Events_Screen, builder: (BuildContext context, GoRouterState state) { return const EventsScreen(); }, ), GoRoute( path: "$EventDetail_Screen/:eventId", builder: (BuildContext context, GoRouterState state) { return EventDetailScreen(eventId: state.params['eventId']!); }, ), ]), ], ); }
问题解答
1. 能否结合StreamBuilder与GoRouter实现认证状态持久化?
完全可以。GoRouter支持通过refreshListenable监听状态变化,同时也能在路由的全局builder或页面内部结合StreamBuilder处理认证加载状态。更推荐的方式是用GoRouter的redirect+refreshListenable机制配合认证Stream实现自动路由跳转,再通过全局StreamBuilder统一处理加载状态。
2. 具体实现方式与代码位置
首先修复RouteNotifier的错误
User类型没有isEmpty/isNotEmpty属性,判断用户是否登录应直接检查currentUser是否为null,同时要避免滥用!运算符防止空指针:
修改后的RouteNotifier:
class RouterNotifier extends ChangeNotifier { RouterNotifier(this._authMethods) { // 监听认证状态变化,触发路由刷新 _authChangesSubscription = _authMethods.authChanges.listen((_) { notifyListeners(); }); } final AuthMethods _authMethods; late final StreamSubscription<User?> _authChangesSubscription; // 判断用户是否已登录 bool get isAuthenticated => _authMethods._auth.currentUser != null; String? redirect(BuildContext context, GoRouterState state) { final isOnAuthScreen = state.location == '/$AuthScreen'; final isOnHomeScreen = state.location == '/$Tabs_Screen'; // 未登录但在首页,跳转到登录页 if (!isAuthenticated && isOnHomeScreen) { return '/$AuthScreen'; } // 已登录但在登录页,跳转到首页 if (isAuthenticated && isOnAuthScreen) { return '/$Tabs_Screen'; } // 其他情况不跳转 return null; } @override void dispose() { _authChangesSubscription.cancel(); super.dispose(); } }
同时修改AuthMethods的user getter,增加空安全:
// 替换原来的User get user => _auth.currentUser!; User? get user => _auth.currentUser;
结合StreamBuilder处理加载状态
在GoRouter的全局builder中使用StreamBuilder统一处理认证状态的等待过程,避免页面闪烁:
更新后的router.dart:
// 先初始化RouterNotifier final routerNotifier = RouterNotifier(AuthMethods()); GoRouter routeConstruct() { return GoRouter( refreshListenable: routerNotifier, // 处理路由跳转逻辑 redirect: routerNotifier.redirect, // 配置错误页面(可选) errorBuilder: (context, state) => const Scaffold(body: Center(child: Text('页面出错'))), routes: <RouteBase>[ // 登录页路由 GoRoute( path: '/$AuthScreen', builder: (context, state) => const AuthScreen(), ), // 首页路由 GoRoute( path: '/$Tabs_Screen', builder: (context, state) => const TabsScreen(), ), // 公共页面路由(无需登录即可访问) GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) { return const PublicScreen(); }, routes: <RouteBase>[ GoRoute( path: Events_Screen, builder: (context, state) => const EventsScreen(), ), GoRoute( path: "$EventDetail_Screen/:eventId", builder: (context, state) => EventDetailScreen(eventId: state.params['eventId']!), ), ], ), ], // 初始加载位置 initialLocation: '/', // 全局builder处理认证加载状态 builder: (context, state, child) { return StreamBuilder<User?>( stream: AuthMethods().authChanges, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } // 认证状态确定后返回对应页面 return child!; }, ); }, ); }
关键逻辑说明
refreshListenable: 传入RouterNotifier,当认证状态变化时,GoRouter自动调用redirect方法刷新路由。redirect: 由RouterNotifier提供,根据用户登录状态和当前页面决定是否跳转。- 全局
builder: 用StreamBuilder包裹所有页面,统一处理认证状态的等待过程,展示加载指示器。
内容的提问来源于stack exchange,提问作者Hendry Lim
相关产品推荐
相关产品推荐

