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

监听Stream后在Bloc中无法触发状态更新问题求助

Flutter上传进度更新异常:StreamController结合Dio触发事件但Widget不重建

在Flutter开发中,使用StreamController配合Dio跟踪图片上传进度,向Stream添加值时能触发事件并发送新状态,但Widget无法正常更新。调试日志显示事件已触发,却看不到UI刷新。

ChatBloc 代码

class Chat extends Bloc<ChatEvent, ChatState> {
  final StreamController<FileDetails> uploadStream =
      StreamController<FileDetails>();

  Chat(this.chatUseCases, this.chat)
      : super(const SingleChatState()) {
    on<ChatEmitUploadList>(_emitUploadList);

    /// listen to changes form upload stream
    uploadStream.stream.listen((event) {
      List<FileDetails> details = state.fileDetailsList;
      details
          .firstWhereOrNull((element) => element.id == event.id)!
          .percentage = event.percentage;
      add(SingleChatEvent.emitUploadList(fileDetails: event));
    });
  }
}

FutureOr<void> _emitUploadList(
    ChatEmitUploadList event, Emitter<SingleChatState> emit) {
  print('emit once');
  List<FileDetails> details = List.from(state.fileDetailsList);
  details
      .firstWhereOrNull((element) => element.id == event.fileDetails.id)!
      .percentage = event.fileDetails.percentage;
  emit(state.copyWith(fileDetailsList: details));
}

BlocBuilder 代码

class UploadingWidget extends StatelessWidget {
  const UploadingWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ChatBloc, ChatState>(
      builder: (context, state) {
        if (state.status == SingleChatStatus.uploadingFiles) {
          return Container(
            decoration: BoxDecoration(
                color: Colors.white, borderRadius: BorderRadius.circular(20.r)),
            margin: const EdgeInsets.all(8),
            padding: const EdgeInsets.all(8),
            child: CustomScrollView(
              shrinkWrap: true,
              slivers: [
                SliverGrid.builder(
                  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                      maxCrossAxisExtent: 100),
                  itemBuilder: (context, index) => Material(
                    child: SizedBox(
                      height: 100,
                      width: 100,
                      child: Center(
                        child: Text(
                            state.fileDetailsList[index].percentage.toString()),
                      ),
                    ),
                  ),
                  itemCount: state.fileDetailsList.length,
                )
              ],
            ),
          );
        }
        return const SizedBox.shrink();
      },
    );
  }
}

Dio 上传进度监听代码

final response = await dio.post(BASE_TEST+sendMessageEndPoint,data: data,
onSendProgress: (count, total) {
  if(file!=null){
    String fileName = file.path.toString().split('/').last;
    double percentage = (count / total * 100).toDouble();
    uploadStream.sink.add(FileDetails(id: file.path!, percentage: percentage,file: file));
  }
},);

调试日志

[log] onEvent -- ChatBloc, ChatEvent.emitUploadList(fileDetails: Instance of 'FileDetails')
[log] onEvent -- ChatBloc, ChatEvent.emitUploadList(fileDetails: Instance of 'FileDetails')
[log] onEvent -- ChatBloc, ChatEvent.emitUploadList(fileDetails: Instance of 'FileDetails')
[log] onEvent -- ChatBloc, ChatEvent.emitUploadList(fileDetails: Instance of 'FileDetails')
[log] onEvent -- ChatBloc, ChatEvent.emitUploadList(fileDetails: Instance of 'FileDetails')

问题原因及修复方案

核心问题

  • 状态不可变性被破坏:FileDetails是可变类,即便用List.from复制列表,列表内的FileDetails对象还是同一引用。修改对象的percentage属性后,Bloc的浅对比无法识别状态变化,导致UI不触发重建。
  • 提前修改状态违反原则:在uploadStream的监听回调里直接修改state中的对象,违背了Bloc状态不可变的核心规则,容易引发状态不一致问题。

修复步骤

  1. 将FileDetails改为不可变类
    用final修饰所有属性,并提供copyWith方法生成新对象:

    class FileDetails {
      final String id;
      final double percentage;
      final File? file;
    
      const FileDetails({required this.id, required this.percentage, this.file});
    
      FileDetails copyWith({double? percentage}) {
        return FileDetails(
          id: id,
          percentage: percentage ?? this.percentage,
          file: file,
        );
      }
    }
    
  2. 修正Bloc状态更新逻辑
    在_emitUploadList中通过替换对象而非直接修改属性来更新列表:

    FutureOr<void> _emitUploadList(
        ChatEmitUploadList event, Emitter<SingleChatState> emit) {
      print('emit once');
      final updatedList = state.fileDetailsList.map((detail) {
        if (detail.id == event.fileDetails.id) {
          return event.fileDetails; // 直接传入新的FileDetails对象
          // 或者用copyWith:return detail.copyWith(percentage: event.fileDetails.percentage);
        }
        return detail;
      }).toList();
      emit(state.copyWith(fileDetailsList: updatedList));
    }
    
  3. 移除Stream监听中对状态的直接修改
    回调里只需要添加事件,不要操作现有状态:

    uploadStream.stream.listen((event) {
      add(SingleChatEvent.emitUploadList(fileDetails: event));
    });
    
  4. 确认状态类copyWith方法正确性
    确保SingleChatState的copyWith返回全新实例,且fileDetailsList是新的列表引用,避免返回原状态。

额外检查点

  • 确认上传过程中SingleChatStatus.uploadingFiles状态保持正确,防止因状态切换导致Widget隐藏。
  • 可以在emit后打印状态的fileDetailsList,验证percentage是否确实更新。

内容的提问来源于stack exchange,提问作者Yaser Hesham

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 14:25:42