Flutter使用Cubit拉取REST API流数据时UI不更新问题
问题背景
基于Flutter BLoC(Cubit)实现REST API数据定时拉取、页面自动同步功能,无需用户手动触发刷新,服务端数据被其他用户修改时,变更内容自动同步到前端页面。
问题现象
- 按照代码设定的5秒定时拉取逻辑,数据仓库层、Cubit层均可正常打印拉取到的最新数据
- 首页的
BlocBuilder和BlocListener仅首次加载时能获取数据、触发页面重建/回调,后续拉取到的更新数据无法被组件接收,页面不会同步刷新
现有实现代码
1. 数据仓库层(data_repository.dart)
//data_repository.dart import 'dart:async'; import 'data.dart'; class DataRepository { final DataApi dataApi; List<Map<String, dynamic>> formatedData = []; final _controller = StreamController<List<Map<String, dynamic>>>(); DataRepository(this.dataApi) { getData(); } Future<void> getData() async { Timer.periodic(Duration(seconds: 5), (timer) async { formatedData.clear(); Map<String, dynamic> res = await dataApi.getData(); List<dynamic> data = res['data']; for (var el in data) { formatedData.add({'id': el['id'], 'name': el['name']}); } _controller.add(formatedData); }); } @override Stream<List<Map<String, dynamic>>> data() async* { yield* _controller.stream; } @override void dispose() => _controller.close(); }
该层通过
_controller的StreamController对外提供数据流,经测试数据拉取、流推送逻辑运行正常。
2. 状态类(data_list_state.dart)
//data_list_state.dart class DataListState extends Equatable { final List<Map<String, dynamic>> data; DataListState(this.data); @override List<Object> get props => [data]; DataListState copyWith({ List<Map<String, dynamic>>? data, }) { return DataListState( data ?? this.data, ); } }
在
copyWith方法中添加打印日志,可正常获取到最新更新数据。
3. Cubit逻辑层(data_list_cubit.dart)
//data_list_cubit.dart class DataListCubit extends Cubit<DataListState> { DataListCubit(this.dataRepository) : super(DataListState([])) { loadList(); } final DataRepository dataRepository; loadList() { dataRepository.data().listen((event) { if (event.isNotEmpty) { emit(state.copyWith(data: dataRepository.formatedData)); } }); } }
在
loadList方法的流监听回调中添加打印日志,可正常获取到最新更新数据。
4. 首页页面(home.dart)
//home.dart class Home extends StatelessWidget { const Home({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column(children: [ BlocListener<DataListCubit, DataListState>( listener: (context, state) { if (state.data.isNotEmpty) print(state.data[0].toString()); }, child: BlocBuilder<DataListCubit, DataListState>( builder: (context, state) { if (state.data.isNotEmpty) print(state.data[0].toString()); return ListView( shrinkWrap: true, children: [ for (Map<String, dynamic> ff in state.data) ListTile( title: Text(ff['name']), leading: Text(ff['id'].toString()), ), ], ); }, ), ), ]), ); } }
页面中
BlocListener和BlocBuilder的打印回调仅首次加载时触发,后续定时拉取的数据更新无法触发回调。
问题根因
核心错误是状态相等性判断失效:
- 现有代码中
DataRepository始终复用同一个formatedData列表实例,每次拉取数据只是先执行clear()清空内容再添加新元素,列表的内存引用始终没有变化 DataListState继承自Equatable,通过props中传入的data列表判断新旧状态是否相等,而Dart中List默认的==是引用相等判断,只要是同一个列表实例,哪怕内容完全不同,也会被判定为相等- Cubit触发
emit时会比对新旧状态,发现两者“相等”就不会向监听器(BlocBuilder/BlocListener)派发更新,因此页面不会重建
同时存在三个潜在隐患:
- 定时任务首次执行需要等待5秒,页面打开后会有5秒空白期
- 流订阅、定时任务没有做取消释放逻辑,容易造成内存泄漏
- 仓库层默认创建的是单订阅流,后续如果多位置监听会直接报错
修复方案
- 每次拉取数据时生成全新的List实例,不复用旧列表对象,确保Equatable能正确识别状态变更
- 将仓库层流改为广播流,支持多位置监听
- 初始化时立即拉取一次数据,避免首屏5秒空白
- 增加订阅、定时任务的取消逻辑,避免内存泄漏
修复后代码如下:
修复后的数据仓库层
//data_repository.dart import 'dart:async'; import 'data.dart'; class DataRepository { final DataApi dataApi; final _controller = StreamController<List<Map<String, dynamic>>>.broadcast(); Timer? _pullTimer; DataRepository(this.dataApi) { // 初始化立即拉取一次数据 _pullData(); // 启动定时拉取任务 _pullTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { await _pullData(); }); } Future<void> _pullData() async { try { Map<String, dynamic> res = await dataApi.getData(); List<dynamic> rawData = res['data']; // 每次拉取生成全新的List实例,不复用旧对象 List<Map<String, dynamic>> formatedData = rawData.map((el) { return {'id': el['id'], 'name': el['name']}; }).toList(); _controller.add(formatedData); } catch (e) { _controller.addError(e); } } Stream<List<Map<String, dynamic>>> data() { return _controller.stream; } void dispose() { _pullTimer?.cancel(); _controller.close(); } }
修复后的Cubit层
//data_list_cubit.dart import 'dart:async'; class DataListCubit extends Cubit<DataListState> { DataListCubit(this.dataRepository) : super(DataListState([])) { _initSubscription(); } final DataRepository dataRepository; StreamSubscription<List<Map<String, dynamic>>>? _dataSub; void _initSubscription() { _dataSub = dataRepository.data().listen((event) { if (event.isNotEmpty) { // 传入新的List实例,确保状态可被识别为变更 emit(state.copyWith(data: List.from(event))); } }); } @override Future<void> close() { // 页面销毁时取消订阅,释放资源 _dataSub?.cancel(); return super.close(); } }
状态类和首页代码无需修改,替换上述两个文件的代码后即可正常实现定时拉取、自动更新页面的效果。
内容的提问来源于stack exchange,提问作者abdello
相关产品推荐
相关产品推荐

