几乎每个Flutter工程师都在生产环境里遇到过经典的无限滚动竞态问题。
用户打开列表,在信号不稳的蜂窝网络上快速滑动,几毫秒内触发多次越过底部阈值的滚动通知。第一次异步HTTP请求还没返回,滚动监听器又触发了。
![]()
结果就是列表重复、页码跳变,或者整个状态机直接卡死。
最近,移动开发者Ali Wajdan发表了一篇讨论度很高的文章,标题叫《3 Lines of Dart async* Code That Fixed My Infinite Scroll Pagination》。他在文中指出了标准分页方案的问题:
"我见过的绝大多数Flutter分页代码,包括我自己写了好几年的,都是把一个可变状态对象包在滚动监听器外面。一个页码计数器、一个loading布尔值、一个hasMore标志,外加一个UI在触底时调用的fetch方法。两个滚动事件挨得近一点,或者一次重建在第一个future完成前又触发了一次加载,它就出问题了……这是典型的竞态条件,而且当状态分散在页码、hasMore和loading三个标志之间、需要保持同步时,情况只会更糟。"
他提出的解法,是把分页逻辑封装进一个Dart的async*生成器,再用StreamIterator消费它。表面上看,把可变状态挪进生成器局部变量确实干净——页码和hasMore标志不再被外部篡改。
Dart的StreamIterator.moveNext()明确不是并发安全的。如果一次快速滑动或双重重建在前一个moveNext()还在等网络返回时又调用了loadNextPage(),Dart会立刻抛出一个未处理的运行时错误:
Bad state: Cannot call moveNext while a previous call to moveNext is still pending.
作者在文章里也承认,他仍然得保留一个手动防护:"我仍然用loadNextPage返回的那个Future来防护UI触发……那个防护是我现在唯一拥有的状态。"
BlocSignal的处理方式则不同:它没有试图在生成器层面硬扛并发,而是把分页状态机本身变成了一个信号驱动的流。滚动事件不再直接触发网络请求,而是先进入一个信号队列,由状态机决定当前是否应该发起下一页请求。这样,无论滚动事件来得有多密集,状态机只会响应它认为合法的那个。
这个思路的本质区别在于:Ali的方案是"让调用方保证不并发调用",而BlocSignal是"让状态机自己处理并发"。前者把责任推给UI层,后者把责任收回到数据层。对于生产环境里那些不可预测的滚动行为,后者的容错性显然要高一个量级。
如果你正在维护一个饱受分页竞态折磨的Flutter应用,与其继续在调用方加各种guard,不如看看BlocSignal是怎么把状态机变成信号驱动的——这可能是比三行代码更彻底的解法。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.