Flutter中基于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会直接复用之前的Cardwidget,不会重新构建 - 新增的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

