Flutter Bloc Listener未随状态变更隐藏加载弹窗问题
Flutter Bloc Consumer Listener未响应:登录后加载弹窗无法隐藏
问题场景
登录页点击登录按钮后,加载弹窗正常显示;登录成功进入NotesView后,加载弹窗未自动隐藏,仅热重启后才消失。尝试以下方案均无效:
- 在
AuthStateLoggedOut的Equatable props中添加loadingText - 在NotesView的
initState中调用LoadingScreen().hide() - 在BlocConsumer的builder中判断
AuthStateLoggedIn状态时调用hide()
核心代码片段
Main.dart
void main() async { WidgetsFlutterBinding.ensureInitialized(); runApp( const CustomTheme( initialThemeKey: ThemeKeys.light, child: HomePage(), ), ); } class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Von Note', theme: CustomTheme.of(context), routes: { createUpdateNoteView: (context) => const CreateUpdateNoteView(), }, home: BlocProvider<AuthBloc>( create: (context) => AuthBloc(FirebaseAuthProvider()), child: const Content()), ); } } class Content extends StatelessWidget { const Content({Key? key}) : super(key: key); @override Widget build(BuildContext context) { context.read<AuthBloc>().add(const AuthEventInitialize()); return BlocConsumer<AuthBloc, AuthState>( listener: (context, state) { if (state.isLoading) { LoadingScreen().show( context: context, text: state.loadingText.toString(), ); } else { LoadingScreen().hide(); } }, builder: (context, state) { if (state is AuthStateLoggedIn) { return const NotesView(); } else if (state is AuthStateNeedVerification) { return const VerifyEmailView(); } else if (state is AuthStateLoggedOut) { return const LoginView(); } else if (state is AuthStateRegistering) { return const RegisterView(); } else { return const Scaffold( body: Splash(), ); } }, ); } }
AuthBloc登录事件处理
on<AuthEventLogIn>((event, emit) async { emit(const AuthStateLoggedOut( exception: null, isLoading: true, loadingText: 'Please wait while I log you in', )); final email = event.email; final password = event.password; try { final user = await provider.logIn( email: email, password: password, ); if (user.isEmailVerified) { emit(const AuthStateLoggedOut( exception: null, isLoading: false, )); emit(AuthStateLoggedIn( user: user, isLoading: false, )); } else { emit( const AuthStateLoggedOut( exception: null, isLoading: false, ), ); emit(const AuthStateNeedVerification(isLoading: false)); } } on Exception catch (e) { emit(AuthStateLoggedOut( exception: e, isLoading: false, )); } });
AuthStateLoggedOut
class AuthStateLoggedOut extends AuthState with EquatableMixin { final Exception? exception; const AuthStateLoggedOut({ required this.exception, required bool isLoading, String? loadingText, }) : super( isLoading: isLoading, loadingText: loadingText, ); @override List<Object?> get props => [exception, isLoading]; }
问题分析
- 冗余状态切换引发Context异常:登录成功后先发射
AuthStateLoggedOut(isLoading: false),再切换到AuthStateLoggedIn,导致BlocConsumer的builder短暂渲染LoginView,原listener的Context因Widget树临时变化,后续AuthStateLoggedIn的隐藏操作失效。 - LoadingScreen实现缺陷:若未通过单例持有OverlayEntry引用,每次调用
LoadingScreen()都会创建新实例,hide操作无法定位到之前显示的弹窗。 - 重复初始化事件:Content为StatelessWidget,build方法可能多次执行,导致重复添加
AuthEventInitialize,引发状态混乱。
解决方案
1. 优化Bloc状态发射逻辑
移除冗余的AuthStateLoggedOut(isLoading: false),直接切换到目标状态:
on<AuthEventLogIn>((event, emit) async { emit(const AuthStateLoggedOut( exception: null, isLoading: true, loadingText: 'Please wait while I log you in', )); final email = event.email; final password = event.password; try { final user = await provider.logIn( email: email, password: password, ); if (user.isEmailVerified) { // 直接发射登录成功状态 emit(AuthStateLoggedIn( user: user, isLoading: false, )); } else { emit(const AuthStateNeedVerification(isLoading: false)); } } on Exception catch (e) { emit(AuthStateLoggedOut( exception: e, isLoading: false, )); } });
2. 修复LoadingScreen单例实现
通过单例持有OverlayEntry,保证show和hide操作对应同一弹窗:
class LoadingScreen { static LoadingScreen? _instance; OverlayEntry? _overlayEntry; LoadingScreen._internal(); factory LoadingScreen() { _instance ??= LoadingScreen._internal(); return _instance!; } void show({required BuildContext context, required String text}) { if (_overlayEntry != null) return; _overlayEntry = OverlayEntry( builder: (context) => Container( color: Colors.black54, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text(text), ], ), ), ), ); Overlay.of(context).insert(_overlayEntry!); } void hide() { _overlayEntry?.remove(); _overlayEntry = null; } }
3. 将Content改为StatefulWidget避免重复初始化
在initState中仅触发一次初始化事件:
class Content extends StatefulWidget { const Content({Key? key}) : super(key: key); @override State<Content> createState() => _ContentState(); } class _ContentState extends State<Content> { @override void initState() { super.initState(); // 仅初始化时触发一次 context.read<AuthBloc>().add(const AuthEventInitialize()); } @override Widget build(BuildContext context) { return BlocConsumer<AuthBloc, AuthState>( listener: (context, state) { print('当前状态: ${state.runtimeType}, isLoading: ${state.isLoading}'); // 调试日志 if (state.isLoading) { LoadingScreen().show( context: context, text: state.loadingText ?? '加载中...', ); } else { LoadingScreen().hide(); } }, builder: (context, state) { if (state is AuthStateLoggedIn) { return const NotesView(); } else if (state is AuthStateNeedVerification) { return const VerifyEmailView(); } else if (state is AuthStateLoggedOut) { return const LoginView(); } else if (state is AuthStateRegistering) { return const RegisterView(); } else { return const Scaffold(body: Splash()); } }, ); } }
4. 确保AuthStateLoggedIn的Equatable配置
检查AuthStateLoggedIn是否正确继承AuthState并将isLoading加入props:
class AuthStateLoggedIn extends AuthState with EquatableMixin { final User user; const AuthStateLoggedIn({ required this.user, required bool isLoading, }) : super(isLoading: isLoading); @override List<Object?> get props => [user, isLoading]; }
验证方法
在BlocConsumer的listener中添加打印日志,确认所有状态变化均被正确接收,且isLoading值符合预期。
内容的提问来源于stack exchange,提问作者Ali Mahfud
相关产品推荐
相关产品推荐

