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 的实现。

相关推荐
千里马学框架9 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台9 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone9 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc10 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo11 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770012 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼13 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
茶底世界之下13 小时前
为什么预览、录制、离线导出不能共享同一个背压策略?
ios·swift
AFinalStone14 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen14 小时前
属性动画原理与高级动画实现
android·kotlin·canvas