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

Flutter中基于RxDart实现流缓存与无限滚动的技术问询

用RxDart实现分页缓存与增量渲染的方案

作为有React Native + Apollo/Redux背景的开发者,刚接触RxDart时确实会有「流状态和缓存怎么结合」的困惑——毕竟Redux是单状态树,Apollo有专门的缓存层,而RxDart的流看起来是“一次性”的数据推送。不过咱们可以用Bloc内维护内存缓存+流推送完整数据集的方式,既符合你熟悉的缓存逻辑,又能让Flutter高效渲染增量内容,完全不用大材小用搞SQLite。

下面分步骤给你实现方案:

1. 改造Bloc,维护内存缓存并推送完整数据集

核心思路是在Bloc里保存一份_cachedPolls作为缓存,每次分页请求后:

  • 首次请求(count和last为null):直接替换缓存
  • 后续分页请求:把新数据追加到缓存中
    然后将完整的缓存列表推送到流里。这样既保留了类似Redux的单一数据源,又能让UI拿到完整的数据集。

修改后的Bloc代码:

class Bloc {
  final Repository _repository = Repository();
  final PublishSubject<List<PollModel>> _polls = PublishSubject<List<PollModel>>();
  // 新增:内存缓存列表
  List<PollModel> _cachedPolls = [];

  Observable<List<PollModel>> get polls => _polls.stream;

  paginatePolls(int? count, String? last) async {
    final List<PollModel> newPolls = await _repository.paginatePolls(count, last);
    if (count == null && last == null) {
      // 首次请求:替换缓存
      _cachedPolls = List.from(newPolls);
    } else {
      // 分页请求:追加新数据到缓存
      _cachedPolls.addAll(newPolls);
    }
    // 推送完整的缓存列表到流中
    _polls.sink.add(List.from(_cachedPolls));
  }

  dispose() {
    _polls.close();
  }
}
final bloc = Bloc();

2. 优化UI层,确保增量渲染而非全量重绘

你担心“每次发送完整列表会重新渲染全量数据”,其实Flutter的ListView.builder结合唯一Key,会自动复用已有item的widget,只渲染新增的部分——这和React的虚拟DOM diff逻辑类似。

修改后的PollScreen代码,重点是给每个Card加上唯一Key:

class PollScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 注意:这里放在build里有风险,建议移到initState(转成StatefulWidget)
    // 避免Flutter多次触发build时重复发起首次请求
    bloc.paginatePolls(null, null);
    return StreamBuilder(
      stream: bloc.polls,
      builder: (context, AsyncSnapshot<List<PollModel>> snapshot) {
        if (!snapshot.hasData || snapshot.data!.isEmpty) {
          return const Text('loading...');
        }
        return ListView.builder(
          itemCount: snapshot.data!.length,
          itemBuilder: (context, int index) {
            final PollModel curItem = snapshot.data![index];
            // 关键:给每个item加唯一Key,帮助Flutter识别已有widget
            return Card(
              key: ValueKey(curItem.id), // 假设PollModel有唯一id字段
              // 你的Render Logic
              child: Text(curItem.title), // 示例渲染内容
            );
          },
        );
      },
    );
  }
}

为什么不会全量重绘?

当流推送新的完整列表时,StreamBuilder会重新执行builder,但ListView.builder会对比新旧列表的item:

  • 已有item的Key不变,且对应的数据没有变化,Flutter会直接复用之前的Card widget,不会重新构建
  • 新增的item是全新的Key,Flutter会构建新的Card并追加到列表底部

完全符合你想要的「仅渲染新增数据」的需求。

3. 额外优化:避免build重复触发首次请求

上面的代码里,bloc.paginatePolls(null, null)放在build方法里有隐患——因为Flutter可能会多次触发build(比如屏幕旋转、父widget更新),导致重复发起首次请求。建议把PollScreen改成StatefulWidget,在initState里只调用一次:

class PollScreen extends StatefulWidget {
  const PollScreen({super.key});

  @override
  State<PollScreen> createState() => _PollScreenState();
}

class _PollScreenState extends State<PollScreen> {
  @override
  void initState() {
    super.initState();
    // 仅在初始化时加载首次数据
    bloc.paginatePolls(null, null);
  }

  @override
  void dispose() {
    // 如果是全局单例Bloc可以不用,但建议根据架构决定是否在这里销毁
    // bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: bloc.polls,
      builder: (context, AsyncSnapshot<List<PollModel>> snapshot) {
        if (!snapshot.hasData || snapshot.data!.isEmpty) {
          return const Text('loading...');
        }
        return ListView.builder(
          itemCount: snapshot.data!.length,
          itemBuilder: (context, int index) {
            final PollModel curItem = snapshot.data![index];
            return Card(
              key: ValueKey(curItem.id),
              child: Text(curItem.title),
            );
          },
        );
      },
    );
  }
}

补充:如果确实想推送增量数据?

如果你坚持只想在流里推送新增的分页数据(而不是完整列表),可以用BehaviorSubject保存当前缓存,然后在UI层合并数据,但这种方式会增加UI层的复杂度,不如上面的方案简洁。举个简单例子:

Bloc里推送增量数据:

// Bloc修改部分
final PublishSubject<List<PollModel>> _newPolls = PublishSubject<List<PollModel>>();
Observable<List<PollModel>> get newPolls => _newPolls.stream;

paginatePolls(int? count, String? last) async {
  final List<PollModel> newPolls = await _repository.paginatePolls(count, last);
  if (count == null && last == null) {
    _cachedPolls = List.from(newPolls);
  } else {
    _cachedPolls.addAll(newPolls);
  }
  _newPolls.sink.add(newPolls); // 只推送新增数据
}

UI层需要自己维护缓存:

// UI层改成StatefulWidget,在state里维护缓存
class _PollScreenState extends State<PollScreen> {
  List<PollModel> _cachedPolls = [];

  @override
  void initState() {
    super.initState();
    bloc.paginatePolls(null, null);
    // 监听增量数据,更新本地缓存
    bloc.newPolls.listen((newPolls) {
      setState(() {
        if (_cachedPolls.isEmpty) {
          _cachedPolls = newPolls;
        } else {
          _cachedPolls.addAll(newPolls);
        }
      });
    });
  }

  // ... 后续build用_cachedPolls渲染列表
}

但这种方式把缓存逻辑分散到了UI层,不符合Bloc的单一数据源原则,所以更推荐第一种方案。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 04:36:36