驯服 Flutter 无限滚动:为何三行 async* 没抓住重点,BlocSignal 如何修复
来源:dev.to — 2026-09-03
📋 概述
Flutter 无限滚动的经典竞态——弱网下快速滑动在毫秒内触发多次越过底部的滚动通知,第一条异步请求还没返回监听器又触发,于是列表重复、页码跳变、状态机锁死。作者剖析一篇广为流传的「三行 Dart async* 治好分页」的文章,指出把 page/hasMore 装进生成器只是表面整洁:Dart 的 StreamIterator.moveNext() 明确不支持并发,仍会抛 `Bad state`,于是作者本人还留着手动 guard——等于换了个抽象而非消除并发。真正的解法是把并发当作事件边界的策略问题,用 BlocSignal 无流的 droppable() 在同步调用帧内丢弃重叠触发。
🔑 核心要点
- `StreamIterator.moveNext()` 并发不安全:快速 fling 或双重重建在上一轮 await 网络时再调它,会直接抛 `Bad state: Cannot call moveNext while a previous call is pending`
- 迭代器是命令式 pull 模型,只吐出原始批次;真实 UI 需要的错误处理、底部 spinner 同时保留旧项、下拉刷新、空态,仍得靠外部状态容器
- 并发该属于事件边界:用户手势或越过阈值的那一刻就声明重叠执行如何处理,而不是埋进拉取循环或 UI 滚动监听里
- `droppable()` 是无流高阶函数:第一事件到达时 `isProcessing` 在同一调用帧内同步翻真,随后同一帧内或请求挂起期间的每个进入事件都被安全丢弃,finally 里归零——零竞态、零微任务延迟、零 Stream 分配
- 列表长度即分页游标:用 `stateValue.posts.length` 做 offset,就没有可失步的 page 计数器、无提前自增、无手动重置
- 搜索词变化用 `restartable()` 自动取消重启进行中的加载,不必手动销毁重建生成器或维护取消令牌
💡 金句
你没有真正消除并发守卫,只是给那层守卫上面再套了个流迭代器抽象。
👍 0
👎 0
← 返回 dev.to 首页