Flutter中BlocListener状态堆叠致SnackBar重复弹出问题排查
Flutter BlocListener 重复触发状态回调问题
我在A页面的Form中设置了一个保存按钮,点击后会触发cubit中的postAssignment操作:
Future<void> postAssignment(List<Assigment?> assignments) async { emit(state.copyWith(status: AssignmentStatus.posting)); try { await _warehouseRepository.postAssignments( assignments.map((a) => AssigmentApi.Assigment.toApi(a!)).toList()); emit(state.copyWith(status: AssignmentStatus.success)); } on Exception catch (e) { print("Error message: ${e.toString()}"); emit(state.copyWith(status: AssignmentStatus.failure)); } }
操作完成后,我希望停留在A页面、重置状态并显示提交成功的snackBar,因此使用了BlocListener:
BlocListener<AssignmentCubit, AssignmentState>( listener: (context, state) async { print('AssignmentCubit-------------->: $state'); if (state.status.isPosting) { AppKeys.NAVIGATOR_KEY.currentState?.pushNamed( SplashPage.routeName, ); } if (state.status.isSuccess) { await context.read<ViewItemCubit>().fetchViewItems( int.parse( context.read<ItemBarcodeCubit>().state, ), ); final snackBar = SnackBar( duration: Duration(seconds: 2), content: Text('added'), backgroundColor: Colors.green, ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } else { AppKeys.NAVIGATOR_KEY.currentState?.popUntil( (route) => route.settings.name == ItemLocatePage.routeName, ); } }, ),
但实际运行中出现状态未重置反而堆叠的问题:
- 提交第一个条目时的日志:
I/flutter (30679): AssignmentCubit-------------->: AssignmentState(AssignmentStatus.posting, []) I/flutter (30679): AssignmentCubit-------------->: AssignmentState(AssignmentStatus.success, [])
- 提交第二个条目时的日志:
I/flutter (30679): AssignmentCubit-------------->: AssignmentState(AssignmentStatus.posting, []) I/flutter (30679): AssignmentCubit-------------->: AssignmentState(AssignmentStatus.posting, []) I/flutter (30679): AssignmentCubit-------------->: AssignmentState(AssignmentStatus.success, []) I/flutter (30679): AssignmentCubit-------------->: AssignmentState(AssignmentStatus.success, [])
后续提交触发的回调次数持续递增,导致snackBar重复弹出。怀疑是Navigator操作导致页面未释放、状态未重置,请问问题出在哪里?
附页面build方法代码:
@override Widget build(BuildContext context) { return WillPopScope( onWillPop: () { AppKeys.NAVIGATOR_KEY.currentState?.pushNamedAndRemoveUntil( HomePage.routeName, (route) => false, ); return Future.value(true); }, child: BlocBuilder<ViewItemFormCubit, ViewItem?>( builder: (context, state) { final dropdownValue = context.read<ViewItemFormCubit>().state == ViewItem.empty() ? null : context.read<ViewItemFormCubit>().state; return MultiBlocListener( listeners: [ BlocListener<ViewItemCubit, ViewItemState>( listener: (context, state) { if (state.status.isLoading) { AppKeys.NAVIGATOR_KEY.currentState?.pushNamed( SplashPage.routeName, ); } else { AppKeys.NAVIGATOR_KEY.currentState?.popUntil( (route) => route.settings.name == ItemLocatePage.routeName, ); } }, ), BlocListener<AssignmentCubit, AssignmentState>( listener: (context, state) async { print('AssignmentCubit-------------->: $state'); if (state.status.isPosting) { AppKeys.NAVIGATOR_KEY.currentState?.pushNamed( SplashPage.routeName, ); } if (state.status.isSuccess) { await context.read<ViewItemCubit>().fetchViewItems( int.parse( context.read<ItemBarcodeCubit>().state, ), ); final snackBar = SnackBar( duration: Duration(seconds: 2), content: Text('added'), backgroundColor: Colors.green, ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } else { AppKeys.NAVIGATOR_KEY.currentState?.popUntil( (route) => route.settings.name == ItemLocatePage.routeName, ); } }, ), ], child: Scaffold( appBar: AppBar( leading: IconButton( icon: Icon(Icons.arrow_back), onPressed: () { _clearAllItemsInList(); AppKeys.NAVIGATOR_KEY.currentState?.pushNamedAndRemoveUntil( HomePage.routeName, (route) => false); }, ), title: Text( 'Locate item ${context.read<ItemBarcodeCubit>().state}'), centerTitle: true, ), floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 40, right: 20), child: FloatingActionButton( onPressed: () => scanBarcodeNormal(dropdownValue), child: Icon( const IconData(0xefe1, fontFamily: 'MaterialIcons'), size: 35.0, ), backgroundColor: Color(0xff33691e), foregroundColor: Colors.white, ), ), body: Center( child: Form( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox( height: 20, ), Padding( padding: const EdgeInsets.all(8.0), child: DropdownButtonFormField<ViewItem>( decoration: InputDecoration( filled: true, labelText: 'Item', ), value: dropdownValue, icon: const Icon(Icons.arrow_downward), elevation: 16, items: context .read<ViewItemCubit>() .state .viewItems ?.map<DropdownMenuItem<ViewItem>>( (item) { return DropdownMenuItem<ViewItem>( value: item, child: Text( '${item.itemId}. ${item.materialNumber} ${item.materialDescription}', ), ); }, ).toList(), onChanged: (ViewItem? newValue) { _clearAllItemsInList(); context.read<ViewItemFormCubit>().set(newValue!); }, ), ), BlocBuilder<AssigmentItemsCubit, AssigmentItemsState>( builder: (context, state) { return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5)), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), spreadRadius: 5, blurRadius: 15, offset: Offset(0, 10), ), ], ), child: dropdownValue != null ? Text( 'Left to assign: ${dropdownValue.totalQuantity - dropdownValue.assignedQuantity - context.read<AssigmentItemsCubit>().state.items.fold( 0, (acc, item) { return acc + item!.quantity; }, )}', style: TextStyle(fontSize: 20), ) : Text(''), ), AssigmentItems( locationListKey: locate_list_key, ), ], ), ); }, ), ElevatedButton( onPressed: () { final assignments = context.read<AssigmentItemsCubit>().state.items; context .read<AssignmentCubit>() .postAssignment(assignments); _clearAllItemsInList(); }, child: Text('Save'), ) ], ), ), ), ), ); }, ), ); }
问题原因分析
- BlocListener重复注册:你的
BlocListener<AssignmentCubit>嵌套在BlocBuilder<ViewItemFormCubit>内部,每次ViewItemFormCubit状态变化触发build时,都会创建新的BlocListener实例并注册到cubit,旧Listener未取消订阅,导致每次提交时所有已注册的Listener都会触发回调,次数递增。 - Navigator操作导致页面栈混乱:在Listener中调用
pushNamed(SplashPage)和popUntil,可能导致当前A页面未被正确销毁,新的A页面实例被重复创建,多个页面实例同时监听同一个AssignmentCubit,加剧重复回调问题。 - Cubit状态未重置:
postAssignment执行完成后,未将状态重置为初始状态(如AssignmentStatus.initial),旧状态残留可能触发额外的回调逻辑。
修复方案
1. 调整BlocListener位置,避免重复注册
将MultiBlocListener移到BlocBuilder<ViewItemFormCubit>外部,确保整个页面生命周期内只注册一次Listener:
@override Widget build(BuildContext context) { return WillPopScope( onWillPop: () { AppKeys.NAVIGATOR_KEY.currentState?.pushNamedAndRemoveUntil( HomePage.routeName, (route) => false, ); return Future.value(true); }, child: MultiBlocListener( listeners: [ // ViewItemCubit的Listener BlocListener<ViewItemCubit, ViewItemState>(...), // AssignmentCubit的Listener BlocListener<AssignmentCubit, AssignmentState>(...), ], child: BlocBuilder<ViewItemFormCubit, ViewItem?>( builder: (context, state) { // 原页面内容 return Scaffold(...); }, ), ), ); }
2. 在Cubit中重置状态
在postAssignment的成功/失败分支末尾,添加状态重置逻辑:
Future<void> postAssignment(List<Assigment?> assignments) async { emit(state.copyWith(status: AssignmentStatus.posting)); try { await _warehouseRepository.postAssignments( assignments.map((a) => AssigmentApi.Assigment.toApi(a!)).toList()); emit(state.copyWith(status: AssignmentStatus.success)); // 重置为初始状态 emit(state.copyWith(status: AssignmentStatus.initial)); } on Exception catch (e) { print("Error message: ${e.toString()}"); emit(state.copyWith(status: AssignmentStatus.failure)); // 重置为初始状态 emit(state.copyWith(status: AssignmentStatus.initial)); } }
3. 优化Navigator操作,改用加载弹窗替代页面跳转
避免在Listener中频繁push页面,改用showDialog显示加载弹窗,防止页面栈混乱:
BlocListener<AssignmentCubit, AssignmentState>( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) async { if (state.status.isPosting) { showDialog( context: context, barrierDismissible: false, builder: (_) => Center(child: CircularProgressIndicator()), ); } if (state.status.isSuccess) { Navigator.of(context).pop(); // 关闭加载弹窗 // 后续snackBar和数据刷新逻辑 } if (state.status.isFailure) { Navigator.of(context).pop(); // 关闭加载弹窗 Navigator.of(context).popUntil(...); } }, ),
4. 使用listenWhen过滤不必要的回调
给BlocListener添加listenWhen参数,只在状态真正变化时触发回调:
BlocListener<AssignmentCubit, AssignmentState>( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { ... }, ),
内容的提问来源于stack exchange,提问作者infinitesimal
相关产品推荐
相关产品推荐

