在我的 Flutter 应用程序中,我使用
go_router
包来管理路由,并使用 Firebase Authentication
进行用户身份验证。我有几个屏幕需要用户经过身份验证才能访问,例如帐户管理、交易和详细信息。
目前,我已经使用
go_router
实现了重定向,以确保未经身份验证的用户被正确重定向。但是,当用户已经位于这些屏幕之一并注销或会话过期时,我面临着挑战。
我正在考虑使用 BlocListener 来检测每个屏幕上身份验证状态的变化,但这似乎会导致代码重复。由于 Firebase 身份验证,我还有一个 Stream 通知身份验证状态的更改,并更新
contex.isUserSignIn
变量。
使用 go_router 和 Firebase 身份验证有效处理 Flutter 中的注销或会话过期事件的最佳实践是什么?
我一直在努力解决完全相同的问题,并研究了几种替代方案和选项。
TL;DR: 选项 3 是我的首选,它使用
GoRouter.refresh()
级别的 main()
方法根据来自 auth 流的事件动态更新 GoRouter 状态。
请参阅此处的示例:https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart
这将顶层应用程序,通常
MyApp
(由 main()
中的 runApp()
返回)包装在 InheritedNotifer 小部件中,他们称之为 StreamAuthScope
,并在通知器 StreamAuthNotifier
和 go_router 的解析管道之间创建依赖关系。当身份验证状态发生变化时(如 MyApp
通过 App
所传达的),这将依次重建 StreamAuthNotifier
(或示例中的 notifyListeners()
)。
我基于 Provider 包实现了一个类似的模型,其中
ChangeProviderNotifier
替换了 StreamAuthScope
并包装了 MyApp
返回的顶级 main()
。然而,这不允许在 Provider.of<>
外壳内创建受监控的 GoRouter( redirect: )
。为了解决这个问题,我创建了一个传入 getRouter
的 isUserSignIn
函数,该函数由 Provider.of<>
主体中但在 MyApp
函数之前的 build
进行监控。这可行,但感觉很麻烦
导致每次身份验证状态更改时都会重建主MyApp
。如果需要,我相信您可以用 BLoC 模型代替 Provider 做类似的事情。
GoRouter
的 refreshListenable:
参数这是基于此 go_router redirection.dart 示例:https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart
在您提到的问题中,您有一个通知身份验证状态更改的流。您可以使用
extends ChangeNotifier
将其包装在类中以使其可监听。然后,在构造函数中,您可以使用 .listen
实例化和监视流,并在每次身份验证状态更改时(可能每次出现流事件时)在附件中发出 notifyListerners()
。在我的例子中,我将这个类称为 AuthNotifier
然后可以将其用作可听的,并使用 GoRouter
的 refreshListenable:
参数,简单地如下: refreshListenable: AuthNotifier()
示例
AuthNotifier
类
class AuthNotifier extends ChangeNotifier {
AuthNotifier() {
// Continuously monitor for authStateChanges
// per: https://firebase.google.com/docs/auth/flutter/start#authstatechanges
_subscription =
FirebaseAuth.instance.authStateChanges().listen((User? user) {
// if user != null there is a user logged in, however
// we can just monitor for auth state change and notify
notifyListeners();
});
} // End AuthNotifier constructor
late final StreamSubscription<dynamic> _subscription;
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
注意:为了避免创建和监视多个流,您需要确保此构造函数仅在您的应用程序中调用一次(在本例中作为
GoRouter
的 refreshListenable:
的一部分),或者将其修改为单例.
GoRouter
的.refresh()
方法与选项 2 类似但更直接的方法是使用
GoRouter
的 .refresh()
方法。这直接调用内部notifyListerners()
来刷新GoRouter配置。我们可以使用与上面的AuthNotifier
类似的类,但我们不需要extends ChangeNotifier
,并且会调用router.refresh()
来代替notifyListeners()
,其中router
是您的GoRouter()
配置。这个新类将在 main()
中实例化。
鉴于其如此简单(2-3行代码),我们也可以跳过类的定义和实例化,直接在
main()
主体中实现功能,如下:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Listen for Auth changes and .refresh the GoRouter [router]
FirebaseAuth.instance.authStateChanges().listen((User? user) {
router.refresh();
});
runApp(
const MyApp(),
);
}
由于这似乎是最直接、最简单的解决方案,因此它是我首选的解决方案,也是我已经实施的解决方案。然而,那里有很多令人困惑和过时的信息,我觉得我没有足够的经验来声称它是任何类型的“最佳实践”,所以将其留给其他人来判断和评论。
我希望所有这些对您和其他人有所帮助,因为我花了很长时间来制定这些不同的选项,并涉猎了各种各样的材料和选项。我觉得绝对有机会改进这方面的官方 go_router 文档!
为了详细说明 Arik_E 的答案,这是我的实现:
我的 main.dart 文件:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Listen for Auth changes and .refresh the GoRouter [router]
GoRouter router = RoutingService().router;
FirebaseAuth.instance.authStateChanges().listen((User? user) {
router.refresh();
});
runApp(App(router: router));
}
class App extends StatelessWidget {
const App({super.key, required this.router});
final GoRouter router;
@override
Widget build(BuildContext context) => MaterialApp.router(
routerConfig: router,
);
}
我有一个包含我的路由器的类:
class RoutingService {
final router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: MainMenuPage.route,
builder: (BuildContext context, GoRouterState state) =>
const MainMenuPage(),
),
GoRoute(
path: LoginPage.route,
builder: (BuildContext context, GoRouterState state) =>
const LoginPage(),
),
],
// redirect to the login page if the user is not logged in
redirect: (BuildContext context, GoRouterState state) async {
final bool loggedIn = FirebaseAuth.instance.currentUser != null;
final bool loggingIn = state.matchedLocation == LoginPage.route;
if (!loggedIn) return LoginPage.route;
if (loggingIn) return MainMenuPage.route;
// no need to redirect at all
return null;
},
);
}
在我的 MainMenuPage 和 LoginPage 中,我定义了路由,以便我可以重用它。只需将
static const route = '/yourRouteName'
添加到小部件即可。