Everything is ViewModel:让状态管理回到对象世界

Everything is ViewModel:让状态管理回到对象世界

做客户端这么多年,Android、Flutter、iOS、RN 都折腾过。各种状态管理方案层出不穷,实际开发绕不开就几件事:状态放哪?页面之间要不要共享实例?什么时候销毁?退出再进来该怎么处理?模块之间怎么拿依赖。

市面上绝大多数状态库,重心都放在数据变化如何通知 UI 刷新。但很容易忽略一件事:承载业务状态的对象,什么时候创建、什么时候回收。

view_model主要解决对象生命周期管理。整体是服务注册 + 引用绑定管理 + 响应式这套组合。它不走 Dagger/Hilt 那种编译期全自动 DI。现在 AI 写代码也多,靠一套简单运行时规则,就能规避生命周期 bug,不用扛重型 DI 的各种负担。

整套体系分成两层:

  1. 响应式层:对象内部状态变更,通知界面刷新;
  2. 对象管理层:控制对象创建、谁在使用、是否共享、何时销毁。

举个很常见的例子:登录会话,首页、个人中心、订单页都要读取登录信息。 状态变化触发页面刷新,是响应式的工作;多个页面是否共用同一个对象、页面关闭要不要关掉网络监听,属于对象管理。单纯的响应式只能管通知,管不了对象的生和死。

响应式只管对象内部数据;view_model管对象完整生命周期。

Everything is ViewModel

这里的意思是:不局限页面,业务里每一个模块都可以写成 ViewModel

页面、详情、弹窗可以是 ViewModel;Repository、业务服务、播放器、后台任务、会话管理,同样可以包装成 ViewModel 交给 Runtime 托管。 它们统一享受同一套能力:响应式状态、引用绑定生命周期、实例隔离 / 共享控制、自动资源回收。

普通 DTO、工具函数、纯工具类不需要托管,正常写就行。

四个核心角色

  • ViewModel:业务对象,可以是页面模块,也可以是数据层、服务层模块;存放状态、执行业务逻辑,持有定时器、网络订阅等资源。
  • Spec:对象注册描述,定义如何构造对象,配置 key 和存活策略。Spec 只是配置,本身不会 new 实例。

属于服务式注册。不像 Hilt 编译扫描自动装配,不会自动注入字段,需要使用者通过 Binding 主动读取 Spec 拿实例。

  • Binding:使用者句柄,可以是页面、弹窗、另一个 ViewModel。每次通过 Binding 读取 Spec,就给目标对象增加一条绑定;Binding 销毁,绑定就移除。
  • Runtime :底层管理器,记录全部绑定关系,做实例查找、创建、销毁。业务代码只调用watch/read,绑定与销毁逻辑全部封装在 Runtime 内部。

Hilt/Dagger 追求依赖全自动装配,但编译慢、上手门槛高、排错麻烦。 view_model选择声明注册 + 运行时绑定,不做全自动注入。规则直白,AI 也能按规则写代码,绝大多数业务场景够用。

对象规则:默认非单例,key 开启共享,绑定决定销毁

  1. 默认不做单例,每次打开页面就新建对象 没有指定 key 的时候,每一次页面拿到的都是全新 new 出来的 ViewModel。 比如打开商品列表 A,筛选选了价格区间;退出去打开商品列表 B,拿到全新实例,A 的筛选条件不会带到 B。
  2. 显式共享:配置相同 Spec + key,多个 Binding 可以绑定同一个实例

就算加了 key,也不等于永久存活。全部绑定释放之后,对象依旧会销毁,尽量少用永久常驻模式。

Runtime 不是简单数字计数,而是记录每一条真实的绑定来源。 拿商品详情页举例:页面本身绑定 ViewModel,页面里面的评论弹窗、收藏子组件也绑定同一个 ViewModel。 用户点返回,页面根 Binding 释放,但评论弹窗还留在界面,弹窗还持有绑定,对象就不能销毁。直到弹窗关闭,所有绑定全部释放,才真正销毁对象,清理轮询、订阅、定时器

带 key 的多实例行为

给 Spec 带上业务 key,不等于一定是单例 。 同一个Spec+key,当所有绑定全部释放、旧对象销毁之后;下次再访问这个 key,会重新 new 一份全新对象。

真实业务例子:

  1. 用户详情 user:123 打开用户 123,new 一份实例;退出页面,所有绑定释放,对象销毁,关掉粉丝数轮询。再次点开 user:123,重新 new 新对象,不会复用上次残留的加载状态。
  2. 订单编辑弹窗 order:456 打开订单 456 编辑,填了一半直接关掉弹窗,绑定全部释放,对象销毁。再次打开这个订单弹窗,又是全新对象,上次填到一半的表单状态全部清空。
  3. 聊天会话 chat:789 进入会话 789,开启消息监听;退出页面,绑定全部释放,对象销毁断开长连接。再进入同一会话,重新 new 实例,重新建连接,旧对象回调不会乱刷界面。

全局登录会话、全局配置这种需要常驻内存的,不要用上面这种行为,用永久单例。

模块依赖:没有父子强所有权

ViewModel 依赖其他托管对象,不要手动 new,也不要手动调用销毁。 每个 ViewModel 内部自带 Dependency Binding,读取别的 Spec 就产生一次绑定。父 ViewModel 销毁,这条绑定随之解除;被依赖对象只要还有别的地方在用,就继续存活;没有任何绑定,自动销毁。

例子:订单详情 ViewModel 依赖订单 Repository。关闭订单详情页,父 ViewModel 销毁,减掉对应绑定;如果还有其他页面在用这个 Repository,它就继续存活。

✅推荐,每次走 Runtime 拿实例

ini 复制代码
SessionViewModel get session => viewModelBinding.read(sessionSpec);

watch 和 read:都会产生绑定

很多人以为 read 只是读个值,不影响生命周期。 实际上watch、read 只要走 Binding 访问,都会产生绑定,参与存活判断

区别:

  • watch:绑定 + 监听状态变更,UI 页面使用;
  • read:仅绑定,不监听,业务方法内部调用。

状态监听 和 对象绑定生命周期,是两套独立逻辑。selector 只管细粒度 UI 更新,不会改变绑定销毁规则。

架构取舍:优先把对象生命周期做稳,细粒度更新是可选优化

很多状态库上来就吹细粒度响应更新。但普通业务页面基本就是 loading/data/error,UI 渲染通常不是瓶颈。

view_model思路很务实:优先搞定对象隔离、共享、销毁这套逻辑。selector 细粒度更新当作可选优化,遇到真实性能问题再打开。

跨平台:统一的是运行规则,不是 API

Flutter、Android、iOS、JS 各端 API 写法不一样,但底层逻辑一致:Spec 描述对象,Binding 维护绑定,key 标记业务身份,全部绑定释放触发销毁;不带 key 默认每次新建对象,带 key 可以共享,但销毁后再访问会重新 new。

配套 AI Skill 固化编码规则,规避生命周期常见坑。

总结

view_model不是全能 DI 容器。 核心:服务注册 + Runtime 引用绑定管理 + 响应式通知。 放弃编译 DI 全自动能力,换更低复杂度,靠运行时绑定保证安全,AI 辅助开发也能产出可靠代码。

复杂度没有消失,收拢到 Runtime 内部。业务写普通业务对象,对象负责业务逻辑;Runtime 通过绑定,统一处理创建、共享、销毁。

业务对象组成业务图,Runtime 维护引用绑定图。

框架可以变,但对象什么时候创建、谁在用、什么时候销毁、退出再进来怎么处理,是客户端通用问题。跨平台统一的不是 API,而是这套对象生命周期模型。

GitHub Repositories

  • flutter_view_model

    Flutter 版本,也是整套设计最早开始和持续演进的实现。

  • apple_view_model

    面向 SwiftUI、UIKit 和 Apple 平台生命周期的实现。

  • android_view_model

    面向 Compose、Activity、Fragment、View 和普通 Kotlin 类的实现。

  • js_view_model

    面向 React Native、Electron 和普通 TypeScript host 的实现。

相关推荐
古法安卓23 分钟前
Android-重启流程源码解析
android·java·android studio
Flutter OH1 小时前
Flutter OH 日志抓取与过滤指南
flutter
Flutter OH1 小时前
Flutter OH 内存与 GPU 问题定位指南
flutter
协议的旁观者1 小时前
Android 高级逆向实战(一):对抗 360 企业加固,Native 抽取还原与 Activity 生命周期重建
android
Crazy_MT2 小时前
不越狱,iPhone 为什么也能安装任意 IPA?聊聊 iLoader、SideStore 和 LiveContainer 的原理
ios·开源
Flutter OH2 小时前
Flutter OH 多设备适配问题定位指南
flutter
律宏阔3 小时前
Flutter Hooks 与 flutter_map_animations 冲突:第二次地图动画报错的解决方案
flutter
mmsx4 小时前
osmdroid 屏幕坐标与测量坐标互转:中心点+比例尺仿射换算
android·源码·地图·osmdroid
00后程序员张4 小时前
Android证书绑定抓包失败?Android SSL Pinning绕过实战指南
android·网络协议·计算机网络·网络安全·adb·https·ssl