You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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'),
                      )
                    ],
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
}

问题原因分析

  1. BlocListener重复注册:你的BlocListener<AssignmentCubit>嵌套在BlocBuilder<ViewItemFormCubit>内部,每次ViewItemFormCubit状态变化触发build时,都会创建新的BlocListener实例并注册到cubit,旧Listener未取消订阅,导致每次提交时所有已注册的Listener都会触发回调,次数递增。
  2. Navigator操作导致页面栈混乱:在Listener中调用pushNamed(SplashPage)和popUntil,可能导致当前A页面未被正确销毁,新的A页面实例被重复创建,多个页面实例同时监听同一个AssignmentCubit,加剧重复回调问题。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 20:19:01