Flutter Web初始认证时跨路由用BlocListener显示加载指示器问题
我正在开发一个使用flutter_bloc进行状态管理、AzureAD实现认证的Flutter Web应用。认证流程在main.dart的runApp(MyApp())之后自动启动,无需用户手动登录,初始配置的相关代码片段如下:
MultiBlocProvider( providers: [ BlocProvider( create: (context) => AuthBloc(authRepository: authRepository)..add(AuthInitializationEvent()), ), ], // Child: MaterialApp and routes setup )
在MaterialApp中我定义了多个路由:
navigatorKey: globalNavigatorKey, routes: { '/': (context) => RootScreen(), '/page1': (context) => Page1(), '/page2': (context) => Page2(), },
在RootScreen中,我使用BlocListener监听认证状态变化:
BlocListener<AuthBloc, AuthState>( listener: (context, state) { if (state.status == AuthStatus.inProgress) { // Intention to show loading indicator } else if (state.status == AuthStatus.authenticated) { globalNavigatorKey.currentState?.pushNamedAndRemoveUntil( '/Page1', (route) => false, ); } }, // Child: BlocBuilder to rebuild UI based on state )
遇到的问题:在初始认证过程中,跨不同路由显示加载指示器(CircularProgressIndicator)时,用户在非根路由(/)刷新页面(F5)或手动导航到非根路由时,加载指示器无法正常显示,但正常导航应用时可以正常显示。
请问如何确保初始认证过程中,无论当前路由或是否刷新页面,都能在所有路由上一致显示加载指示器?另外,考虑到认证流程是自动触发的,如何有效构建BlocListener和BlocBuilder来处理该场景?
1. 将加载指示器提升到全局层级
问题核心是当前加载逻辑仅绑定在RootScreen,非根路由刷新时不会渲染该页面,自然无法感知认证状态。解决思路是把加载指示器放在MaterialApp的父级,确保所有路由场景下都能响应认证状态:
修改MultiBlocProvider的子组件结构,用BlocBuilder包裹MaterialApp,结合Stack实现全局加载层覆盖:
MultiBlocProvider( providers: [ BlocProvider( create: (context) => AuthBloc(authRepository: authRepository)..add(AuthInitializationEvent()), ), ], child: BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { return Stack( children: [ MaterialApp( navigatorKey: globalNavigatorKey, routes: { '/': (context) => RootScreen(), '/page1': (context) => Page1(), '/page2': (context) => Page2(), }, ), // 根据认证状态显示全局加载指示器 if (state.status == AuthStatus.inProgress) Container( color: Colors.black54, child: const Center( child: CircularProgressIndicator(), ), ), ], ); }, ), )
2. 重构全局认证状态监听逻辑
原来的BlocListener仅在RootScreen中,非根路由刷新时无法触发页面跳转。需要把路由跳转逻辑移到MaterialApp层级的BlocListener中,确保任何路由下都能响应认证完成事件:
MultiBlocProvider( providers: [ BlocProvider( create: (context) => AuthBloc(authRepository: authRepository)..add(AuthInitializationEvent()), ), ], child: BlocListener<AuthBloc, AuthState>( listener: (context, state) { if (state.status == AuthStatus.authenticated) { globalNavigatorKey.currentState?.pushNamedAndRemoveUntil( '/page1', (route) => false, ); } // 可添加未认证状态的处理逻辑,比如跳转到登录页(如果需要) }, child: BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { return Stack( children: [ MaterialApp( navigatorKey: globalNavigatorKey, routes: { '/': (context) => RootScreen(), '/page1': (context) => Page1(), '/page2': (context) => Page2(), }, ), if (state.status == AuthStatus.inProgress) Container( color: Colors.black54, child: const Center( child: CircularProgressIndicator(), ), ), ], ); }, ), ), )
3. 处理非根路由初始化时的认证状态
Flutter Web刷新或手动导航到非根路由时,会直接初始化对应页面,此时需要在每个页面的初始化阶段检查认证状态,避免未完成认证就显示页面内容:
class Page1 extends StatefulWidget { @override State<Page1> createState() => _Page1State(); } class _Page1State extends State<Page1> { @override void initState() { super.initState(); final authState = context.read<AuthBloc>().state; if (authState.status != AuthStatus.authenticated) { // 未认证时触发重新初始化 context.read<AuthBloc>().add(AuthInitializationEvent()); } } @override Widget build(BuildContext context) { return BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { if (state.status != AuthStatus.authenticated) { // 未完成认证时返回空布局,依赖全局加载层 return const SizedBox.shrink(); } // 页面正常内容 return Scaffold( appBar: AppBar(title: const Text('Page1')), body: const Center(child: Text('Authenticated Content')), ); }, ); } }
4. 优化认证事件触发逻辑
在AuthBloc中添加状态判断,避免重复触发初始化事件:
class AuthBloc extends Bloc<AuthEvent, AuthState> { final AuthRepository authRepository; AuthBloc({required this.authRepository}) : super(AuthState(status: AuthStatus.unauthenticated)) { on<AuthInitializationEvent>(_onAuthInitialization); } Future<void> _onAuthInitialization(AuthInitializationEvent event, Emitter<AuthState> emit) async { // 正在初始化时直接返回,避免重复请求 if (state.status == AuthStatus.inProgress) return; emit(state.copyWith(status: AuthStatus.inProgress)); try { final user = await authRepository.initializeAuth(); emit(state.copyWith(status: AuthStatus.authenticated, user: user)); } catch (e) { emit(state.copyWith(status: AuthStatus.unauthenticated, error: e.toString())); } } }
内容的提问来源于stack exchange,提问作者Fabian M

