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

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秒空白期
  • 流订阅、定时任务没有做取消释放逻辑,容易造成内存泄漏
  • 仓库层默认创建的是单订阅流,后续如果多位置监听会直接报错
修复方案
  1. 每次拉取数据时生成全新的List实例,不复用旧列表对象,确保Equatable能正确识别状态变更
  2. 将仓库层流改为广播流,支持多位置监听
  3. 初始化时立即拉取一次数据,避免首屏5秒空白
  4. 增加订阅、定时任务的取消逻辑,避免内存泄漏

修复后代码如下:

修复后的数据仓库层

//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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:30:56