AsyncRedux流与计时器管理Skill:把流与计时器放进存储属性,让状态管理不再漏内存
- 一句话定位:这是一份专为 Flutter + AsyncRedux 项目准备的工程实践技能,教你用「存储属性(Props)」统一托管
Stream订阅与Timer计时器,避免内存泄漏与生命周期错乱。 - 核心规则只有两条:不把流/计时器传进 Widget,也不把流/计时器塞进 Redux 状态;它们属于「属性」而非「状态」。
- 提供完整 API 路径:
setProp/prop<T>/disposeProp/disposeProps,覆盖启动、读取、单条清理与批量清理。 - 覆盖真实工程场景:轮询计时器、实时价格流、屏幕级订阅、应用级订阅、单动作切换启停,以及
ConnectorTester生命周期测试。 - 收益可感知:把「谁创建、谁销毁」的隐式契约变成显式动作,页面关闭即释放,存储关闭统一兜底。
该技能叫什么?如何一键安装 AI 技能提升开发效率?
技能中文名:AsyncRedux流与计时器管理Skill 技能英文名:asyncredux-streams-timers 技能ID:11676
它要解决的问题非常具体:在 AsyncRedux(Flutter 的状态管理库)项目里,数据流(Stream)与计时器(Timer)天生是「有生命周期、需要释放」的对象。很多开发者习惯在 Widget 里 listen、在 initState 里 Timer.periodic,结果就是页面销毁后回调仍在触发、setState 报错、订阅层层堆叠。这个技能的价值在于给出了一套被验证过的落地范式——把这类对象存进 Store 的属性容器,用 ReduxAction 启动与停止,用 disposeProps() 兜底回收。
关于「一键安装」:该技能以技能包形式分发,安装入口与具体命令请以官方说明为准(不同客户端的技能市场/技能库入口不同)。通用流程可以这样理解:
- 确认前提:本地已有可用的 AI 编程助手客户端(支持技能/Skill 机制),项目为 Flutter 工程且已引入 AsyncRedux 依赖。
- 获取技能:在技能市场或技能库中按中文名「AsyncRedux流与计时器管理」或英文名
asyncredux-streams-timers检索,也可通过技能 ID11676定位(具体检索方式以官方说明为准)。 - 安装启用:点击安装/启用按钮,将技能加载到当前会话或工作区;若客户端支持本地技能目录,也可按官方文档放置技能文件。
- 验证是否生效:向助手提问「在 AsyncRedux 里如何启动一个每 5 秒轮询的计时器并在页面销毁时停止?」,若回答中出现
setProp、disposeProp、ReduxAction等关键词,并强调「不要把计时器放进 Widget 或状态里」,即说明技能已正确加载。 - 持续使用:后续让助手直接生成动作类、审查现有代码中的订阅泄漏点,都属于该技能的作用范围。
需要强调:技能本身只是知识与范式载体,不改变你项目的依赖配置;AsyncRedux 的版本差异与 API 细节请以官方说明为准。
该技能的主要功能和特点
- 明确「属性 vs 状态」的边界:技能反复强调,流与计时器会产生状态变化,但它们本身不是状态。放进状态会导致状态不可序列化、时间旅行调试失真;放进属性容器才是正确姿势。
- 提供成对的启停动作范式:
StartXxxAction/StopXxxAction成对出现,启动动作里setProp,停止动作里disposeProp,让「创建」与「销毁」在代码结构上对称可查。 - 自动化的资源回收语义:
disposeProps()会自动取消Timer、取消StreamSubscription、关闭StreamController与StreamSink,并且忽略Future(避免未处理异常),这一点比手写cancel()更省心。 - 支持谓词过滤的批量清理:可以只清理计时器(
value is Timer),也可以按键名前缀清理(如key.toString().startsWith('temp_')),适合临时资源与长驻资源混用的项目。 - 覆盖完整生命周期分层:屏幕级用
StoreConnector的onInit/onDispose;应用级在Store创建后启动、关闭前停止;还给出「单动作组合启停」的TogglePollingAction写法。 - 可测试:提供
ConnectorTester的runOnInit()/runOnDispose()用法,配合store.waitAnyActionTypeFinishes,不用启动完整 Widget 树就能验证生命周期行为。 - 与普通做法的差异:普通工具只告诉你「记得 cancel」,这个技能告诉你「cancel 应该发生在哪个动作里、由谁触发、如何被测试覆盖」。
如何使用该技能?
下面按「从零到跑通」的顺序给出使用步骤,代码均为 Dart。
第 1 步:理解两类对象的存放位置。 开始写代码前先自查:当前这段流/计时器代码是在 Widget 里,还是在状态类里?如果答案是「是」,就要迁移到属性容器。
第 2 步:用 setProp 存放对象。 在 ReduxAction 的 reduce() 中创建并存入:
class StartPollingAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
setProp('pollingTimer', Timer.periodic(
Duration(seconds: 5),
(timer) => dispatch(FetchDataAction()),
));
return null; // 此动作无状态变化
}
}
第 3 步:用 prop<T> 读取。 需要拿到计时器或订阅时,按键取值并指定泛型:
var timer = prop<Timer>('myTimer');
var subscription = prop<StreamSubscription>('dataSubscription');
第 4 步:用 disposeProp 停止单个资源。 停止动作保持极简:
class StopPollingAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
disposeProp('pollingTimer');
return null;
}
}
第 5 步:订阅流并把订阅存起来。 流回调里不要去改状态,而是 dispatch 一个新动作:
class StartListeningAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
final subscription = myDataStream.listen(
(data) => dispatch(DataReceivedAction(data)),
onError: (error) => dispatch(StreamErrorAction(error)),
);
setProp('dataSubscription', subscription);
return null;
}
}
第 6 步:把生命周期挂到屏幕上。 使用 StoreConnector 的 onInit / onDispose,页面进入即启动、离开即停止:
StoreConnector<AppState, _Vm>(
vm: () => _Factory(),
onInit: (store) => store.dispatch(StartPriceStreamAction()),
onDispose: (store) => store.dispatch(StopPriceStreamAction()),
builder: (context, vm) => PriceWidget(price: vm.price),
)
第 7 步:应用级资源在 main 中启动,关闭前清理。 顺序很重要:先 disposeProps(),再 shutdown()。
store.dispatch(StopGlobalPollingAction());
store.disposeProps(); // 清理所有剩余属性
store.shutdown();
第 8 步:加一层幂等保护。 启动动作里先判断 state.isStreaming,已开启就直接 return null;停止动作里判断未开启就直接返回,避免重复启停。
第 9 步:写一个生命周期测试。 用 ConnectorTester 模拟页面进出:
var connectorTester = store.getConnectorTester(PriceScreen());
connectorTester.runOnInit();
await store.waitAnyActionTypeFinishes([StartPriceStreamAction]);
expect(store.state.isStreaming, true);
connectorTester.runOnDispose();
await store.waitAnyActionTypeFinishes([StopPriceStreamAction]);
expect(store.state.isStreaming, false);
输入与输出说明:输入是「流对象 / 计时器对象 + 一个键名」;输出是可被 disposeProp 精确回收的属性条目,以及被 dispatch 出来的状态更新动作。键名建议语义化并保持唯一,如 priceSubscription、pollingTimer。
同类型竞品对比分析
| 方案名称 | 定位 | 核心功能 | 安装/使用门槛 | 适用场景 | 优势与不足 |
|---|---|---|---|---|---|
| AsyncRedux流与计时器管理Skill(本技能) | AsyncRedux 体系内的资源生命周期规范 | setProp/prop<T>/disposeProp/disposeProps,动作化启停,谓词批量清理,ConnectorTester 测试 |
需项目已使用 AsyncRedux;需理解 Action 与 Store 概念 | 中大型 Flutter 项目、需要可测试与可追溯的资源管理 | 优势:启停对称、兜底清理、可测试;不足:绑定 AsyncRedux,迁移成本高,纯 Widget 层项目用不上 |
| 原生 StatefulWidget 手动管理 | Flutter 内置做法 | initState 创建、dispose 中 cancel() |
零门槛,无需额外依赖 | 单页面、资源少的小型功能 | 优势:直观、无学习成本;不足:跨页面共享困难,容易漏写 cancel,无法集中治理 |
| Provider / Riverpod 的销毁回调 | 依赖注入层的生命周期钩子 | ref.onDispose、Provider 的 dispose 回调 |
需引入对应依赖并理解其作用域模型 | 需要依赖注入与作用域隔离的项目 | 优势:作用域清晰、生态成熟;不足:资源与 Redux 状态割裂,跨模块复用需额外封装 |
Bloc 的 close() 与 emit.forEach |
事件驱动状态管理 | 在 Bloc 内部声明订阅并在 close 中释放 |
需切换到 Bloc 架构 | 事件流密集、强调单向数据流的项目 | 优势:与流天然契合;不足:架构迁移成本大,计时器等副作用仍需自行托管 |
选型建议:项目已经在用 AsyncRedux,就直接采用本技能的属性容器方案,收益最大;如果项目尚未引入 AsyncRedux,仅为管理几个计时器而迁移框架并不划算,可先用原生 dispose 兜底。
该技能的典型应用场景与实际体验情况
场景一:行情类页面的实时价格推送。 角色是 Flutter 客户端开发。痛点是用户反复进出报价页后,旧的 priceStream 订阅仍在推送,导致价格跳变、内存上涨。使用方式是页面 onInit 派发 StartPriceStreamAction、onDispose 派发 StopPriceStreamAction,并在启动动作里用 state.isStreaming 做幂等判断。实际体验:改写量很小,主要工作是把原来的 listen 搬进 Action;一旦搬完,「进出页面不泄漏」几乎不需要再额外操心,出错时还有 PriceStreamErrorAction 统一收口并停止流。
场景二:定时轮询刷新数据。 角色是后台管理类应用开发。痛点是多个列表页都需要 5 秒轮询,但忘记停止会造成请求风暴。使用方式是 StartPollingAction + StopPollingAction,或者用 TogglePollingAction(start: bool) 一个动作搞定开关。实际体验:disposeProps(({key, value}) => value is Timer) 这条谓词清理在调试阶段特别好用,可以在开发面板上一键清空所有计时器,快速复现或排除问题。
场景三:需要计数的倒计时/秒表。 角色是做活动页的开发。使用 Timer.periodic 的 timer.tick,在回调里 dispatch(UpdateTickAction(timer.tick)),把计数写进状态。实际体验:状态里只存数字、不存计时器,时间旅行调试和状态快照都保持干净,这是「属性 vs 状态」边界带来的直接好处。
场景四:应用级全局推送与团队协作审查。 角色是团队 Tech Lead。使用方式是在 main 中创建 Store 后启动全局动作,关闭前统一 disposeProps() + shutdown(),并把「流/计时器禁止出现在 Widget 与状态中」写进代码评审清单。实际体验:ConnectorTester 让生命周期行为可以被单元测试覆盖,评审时不必靠肉眼找 cancel() 是否遗漏。
上手难度评估:中等偏低。只要接受「动作负责启停、属性负责存放」这一条心智模型,剩下的都是模板化代码;真正的坑在于键名散落各处容易拼错,建议用常量集中管理键名。
该技能能为用户带来的价值
- 消除一类高频线上问题:订阅未取消、计时器未停止是 Flutter 应用内存增长与「页面已销毁仍回调」的主要来源之一。把创建与销毁都收进动作,等于把这类问题从「靠自觉」变成「靠结构」。
- 降低协作与交接成本:新同学接手时,搜索
StartXxxAction/StopXxxAction就能看清一个资源的完整生命周期,不需要通读整棵 Widget 树。 - 缩短排障时间:
disposeProps()提供统一的兜底回收,存储关闭时一次性清理所有Timer、StreamSubscription、StreamController、StreamSink,并安全忽略Future,避免「清理代码本身又抛异常」的二次故障。 - 提升可测试性:
ConnectorTester+waitAnyActionTypeFinishes让「页面进出是否正确启停」成为可断言的测试用例,把生命周期从人工验证变成 CI 可回归项。 - 长期价值:这套「副作用托管在属性、状态只存纯数据」的思路可以延伸到 WebSocket 长连接、传感器订阅、动画控制器等更多场景,属于一次学习、长期复用的工程习惯。
常见问题 FAQ
Q1:这个技能怎么安装?安装后需要改项目依赖吗?
A:通过支持技能机制的 AI 编程助手客户端,在技能市场或技能库中按中文名「AsyncRedux流与计时器管理」、英文名 asyncredux-streams-timers 或技能 ID 11676 检索并启用即可,具体入口与命令以官方说明为准。它只提供知识与代码范式,不会自动为你添加 AsyncRedux 依赖,依赖仍需按 AsyncRedux 官方文档自行配置。
Q2:这个技能收费吗? A:技能本身的计费方式取决于你使用的技能分发平台与账号套餐,请以官方说明为准。技能内容基于 AsyncRedux 的公开文档整理,不包含额外的付费服务。
Q3:为什么不能把计时器直接放进 Redux 状态?放进状态不是更方便吗?
A:因为 Timer、StreamSubscription 这类对象不是「状态」,它们只是产生状态变化的副作用载体。放进状态会破坏状态的可序列化与可比对特性,影响调试与持久化,也让「谁负责销毁」变得模糊。技能给出的答案是:状态里只存 price、isStreaming 这样的纯数据,对象本身放属性容器。
Q4:disposeProp 和 disposeProps 该怎么选?
A:明确知道要停哪一个就用 disposeProp('key'),精确、可读;需要在页面或应用关闭时统一兜底,就用 disposeProps(),它会自动处理 Timer、StreamSubscription、StreamController、StreamSink 并忽略 Future。也可以用谓词做定向批量清理,例如只清计时器或只清某个键名前缀。
Q5:它和 Provider / Riverpod、Bloc 这些方案怎么选?
A:看你项目现有的状态管理底座。已经在用 AsyncRedux,本技能是成本最低、最贴合的选择;如果项目用 Riverpod,就用 ref.onDispose 完成同类职责;如果团队偏好事件驱动架构,Bloc 的 close() 也能承担。核心原则是一致的:副作用对象要有明确归属和确定销毁点,差别只在于挂在哪套机制上。