Bloc重复触发同一状态的原因及代码修复方案
问题
我是Bloc新手,发现单次状态变更会触发多次状态变化,比如执行登录或登出操作时会弹出多个提示Snackbar。操作功能正常,但登出时会连续弹出5个“Logged out successfully”提示。以下是我的代码实现:
login_page.dart中的BlocListener
BlocListener<AuthBloc, AuthState>( listenWhen: (previous, current) => previous != current, listener: (context, state) { dev.log('listener login page: $state'); if (state is AuthLoadingState) { loadingDialog(context); } else if (state is AuthLoadedState) { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => const HomePage(), ), ); showSnackBar( message: 'Logged in successfully', isError: false); } else if (state is AuthErrorState) { Navigator.of(context).pop(); showSnackBar(message: state.message, isError: true); } }, child: AuthButtonWidget( formKey: _formKey, onPressed: () { if (_formKey.currentState!.validate()) { BlocProvider.of<AuthBloc>(context).add( LogInWithEmailAndPasswordEvent( email: _emailController.text, password: _passwordController.text, ), ); } }, text: 'Log In', ), ),
home_page.dart中的BlocConsumer
BlocConsumer<AuthBloc, AuthState>( listenWhen: (previous, current) => previous != current, listener: (context, state) { dev.log('listener home page: $state'); if (state is AuthLoadingState) { loadingDialog(context); } else if (state is AuthLoggedOutState) { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => const LoginPage(), ), ); showSnackBar(message: 'Logged out successfully', isError: false); } else if (state is AuthErrorState) { Navigator.of(context).pop(); showSnackBar(message: state.message, isError: true); } }, buildWhen: (previous, current) => previous != current, builder: (context, state) { dev.log('builder home page: $state'); if (state is AuthLoadedState) { return const HomePageWidget(); } else if (state is AuthErrorState) { return const Center( child: Text('AuthErrorState'), ); } else { return const HomePageWidget(); } }, );
AuthBloc实现
class AuthBloc extends Bloc<AuthEvent, AuthState> { final SignUpWithEmailAndPasswordUsecase signUpWithEmailAndPassword; final LogInWithEmailAndPasswordUsecase logInWithEmailAndPassword; final LogOutUsecase logOut; AuthBloc( this.signUpWithEmailAndPassword, this.logInWithEmailAndPassword, this.logOut, ) : super(AuthInitial()) { on<AuthEvent>((event, emit) async { if (event is SignUpWithEmailAndPasswordEvent) { emit(AuthLoadingState()); final failureOrUnit = await signUpWithEmailAndPassword( email: event.email, password: event.password, name: event.name, ); emit(_mapFailureOrPostsState(either: failureOrUnit)); } else if (event is LogInWithEmailAndPasswordEvent) { emit(AuthLoadingState()); final failureOrUnit = await logInWithEmailAndPassword( email: event.email, password: event.password, ); emit(_mapFailureOrPostsState(either: failureOrUnit)); } else if (event is LogOutEvent) { emit(AuthLoadingState()); final failureOrUnit = await logOut(); emit(_mapFailureOrPostsState( either: failureOrUnit, isLogOut: true, )); } }); } } AuthState _mapFailureOrPostsState({ required Either<Failure, Unit> either, bool isLogOut = false, }) { return either.fold( (failure) => AuthErrorState(message: _mapFailureToMessage(failure)), (_) { if (isLogOut) { return AuthLoggedOutState(); } else { return AuthLoadedState(); } }, ); } String _mapFailureToMessage(Failure failure) { if (failure is SignUpWithEmailAndPasswordFailure) { switch (failure.code) { case 'email-already-in-use': return EMAIL_ALREADY_IN_USE_FAILURE_MESSAGE; case 'invalid-email': return INVALID_EMAIL_FAILURE_MESSAGE; case 'operation-not-allowed': return OPERATION_NOT_ALLOWED_FAILURE_MESSAGE; case 'weak-password': return WEAK_PASSWORD_FAILURE_MESSAGE; default: return SIGN_UP_WITH_EMAIL_AND_PASSWORD_FAILURE_MESSAGE; } } else if (failure is LogInWithEmailAndPasswordFailure) { switch (failure.code) { case 'invalid-email': return INVALID_EMAIL_FAILURE_MESSAGE; case 'user-disabled': return USER_IS_DISABLED_FAILURE_MESSAGE; case 'user-not-found': return USER_NOT_FOUND_FAILURE_MESSAGE; case 'wrong-password': return WRONG_PASSWORD_FAILURE_MESSAGE; default: return LOG_IN_WITH_EMAIL_AND_PASSWORD_FAILURE_MESSAGE; } } else if (failure is LogOutFailure) { return LOG_OUT_FAILURE_MESSAGE; } else if (failure is OfflineFailure) { return OFFLINE_FAILURE_MESSAGE; } else if (failure is EmptyCacheFailure) { return EMPTY_CACHE_FAILURE_MESSAGE; } else { return UNEXPECTED_FAILURE_MESSAGE; } }
原因分析
- 多监听者重复响应:登录页的
BlocListener和首页的BlocConsumer都监听同一个AuthBloc,页面跳转后旧页面的监听者未被正确销毁,仍会接收状态变更触发回调。 - listenWhen条件太宽松:
previous != current会监听所有状态变化,包括Loading到最终状态的切换,页面跳转后旧监听者仍会响应后续状态,重复执行showSnackBar。 - Widget重建导致监听重复注册:如果
BlocListener/BlocConsumer所在Widget频繁重建,会多次注册监听,每次重建都会触发状态回调。
修复方案
1. 精准控制listenWhen条件
只在目标状态首次触发时执行回调,避免不必要的响应:
登录页BlocListener修改
BlocListener<AuthBloc, AuthState>( listenWhen: (previous, current) { // 只监听Loading、Loaded、Error状态的首次切换 return (current is AuthLoadingState && previous is! AuthLoadingState) || (current is AuthLoadedState && previous is! AuthLoadedState) || (current is AuthErrorState && previous is! AuthErrorState); }, listener: (context, state) { dev.log('listener login page: $state'); // 原有逻辑不变 }, // 其余代码不变 ),
首页BlocConsumer修改
BlocConsumer<AuthBloc, AuthState>( listenWhen: (previous, current) { // 只监听Loading、LoggedOut、Error状态的首次切换 return (current is AuthLoadingState && previous is! AuthLoadingState) || (current is AuthLoggedOutState && previous is! AuthLoggedOutState) || (current is AuthErrorState && previous is! AuthErrorState); }, listener: (context, state) { dev.log('listener home page: $state'); // 原有逻辑不变 }, // buildWhen保持或按需调整 buildWhen: (previous, current) => previous != current, builder: (context, state) { // 原有逻辑不变 }, );
2. 确保SnackBar上下文正确
使用当前页面的ScaffoldMessenger显示SnackBar,避免上下文错误导致重复弹出:
// 修改showSnackBar实现 void showSnackBar(BuildContext context, {required String message, required bool isError}) { final snackBar = SnackBar( content: Text(message), backgroundColor: isError ? Colors.red : Colors.green, ); ScaffoldMessenger.of(context).showSnackBar(snackBar); }
调用时传入当前上下文:showSnackBar(context, message: 'Logged out successfully', isError: false);
3. 优化Bloc作用域(可选)
如果AuthBloc仅用于登录/首页流程,可在登录页父级提供Bloc,而非全局提供,页面跳转后Bloc会被销毁,避免旧监听者残留。
内容的提问来源于stack exchange,提问作者Mikelenjilo
相关产品推荐
相关产品推荐

