别再造「上帝ViewModel」了:扔掉「一个屏幕=一个ViewModel」的旧观念
利用Android全新ViewModel API,打造可感知生命周期的Compose架构
(AI生成配图)
为什么这很重要
在传统Android开发中,ViewModel的作用域通常与Activity、Fragment或导航目的地绑定。
但在如今的Jetpack Compose应用中,单个屏幕往往包含大量动态元素:
- 动态显隐的UI组件
- 多层嵌套可组合项
- 滑动分页器(Pager)
- 底部抽屉
- 临时功能流程
这种场景下,把所有内容的作用域都绑定到整个屏幕就过于宽泛了。
很多时候你只希望ViewModel的生命周期和某个特定的可组合项保持一致——这正是全新ViewModel API要解决的核心问题。
传统ViewModel作用域的痛点
普通的viewModel()调用会默认使用最近的ViewModelStoreOwner,通常的作用域层级是:
Activity
└─ NavBackStackEntry
└─ 整个屏幕
这会导致一系列问题:
- ViewModel存活时间远超实际需求
- 临时UI状态长期占用内存
- 分页器的不同页面意外共享状态
- 资源清理时机不明确
在高度动态的Compose UI中,这些问题会被无限放大。
官方参考
ViewModel 作用域 API | Android 开发者官方文档ViewModel 可帮助你以可感知生命周期的方式管理UI数据
1. 将ViewModel绑定到特定可组合项的作用域
第一个新API可以将ViewModel的生命周期直接与可组合项的调用位置绑定。
示例代码
@Composable
fun RememberViewModelStoreOwnerSample() {
// 创建与当前调用位置绑定的ViewModelStoreOwner
// 当该可组合项退出组合树时,关联的ViewModelStore会被自动清理
val scopedOwner = rememberViewModelStoreOwner() // 核心API
CompositionLocalProvider(LocalViewModelStoreOwner provides scopedOwner) {
// 这个ViewModel的作用域绑定到上面的scopedOwner
// 支持配置变更(如旋转屏幕)时存活,但可组合项移除后会立即清理
val viewModel = viewModel { TestViewModel("scoped_data") }
// 后续使用ViewModel的逻辑
}
}
内部运行逻辑
作用域层级如下:
屏幕
└─ RememberViewModelStoreOwnerSample()
└─ rememberViewModelStoreOwner()
└─ LocalViewModelStoreOwner
└─ TestViewModel
核心触发流程:
可组合项退出组合
↓
ViewModelStore被清理
↓
ViewModel.onCleared() 被调用
这给Compose补上了一直缺失的能力:可组合项层级的ViewModel所有权。
能力优势
(AI生成配图:全局作用域与可组合项作用域的生命周期对比)
现在ViewModel的生命周期和UI本身完全对齐,不需要再让状态为整个屏幕持续存活。
生命周期可视化
时间轴 ──────────────────────────────────────────▶
屏幕 ██████████████████████████████████████████
可组合项 ████████████████
ViewModel ████████████████
ViewModel仅在可组合项存在的时间段存活,这种生命周期对齐是最核心的改进。
2. 为分页器、标签页、懒加载列表创建独立ViewModel
第二个API对于分页场景的优化更为实用。
示例代码
@Composable
fun RememberViewModelStoreProviderSample() {
val storeProvider = rememberViewModelStoreProvider() // 核心API
val pages = listOf("页面1", "页面2", "页面3")
HorizontalPager(pageCount = pages.size) { page ->// 借助Provider为当前页面创建专属的ViewModelStoreOwner
val pageOwner = rememberViewModelStoreOwner(
provider = storeProvider,
key = page
)
CompositionLocalProvider(LocalViewModelStoreOwner provides pageOwner) {
val pageViewModel = viewModel {
TestViewModel(pages[page])
}
// 后续使用页面专属ViewModel的逻辑
}
}
}
传统分页器的固有问题
没有页面级作用域ViewModel时,所有页面共享同一个ViewModel:
分页器
├─ 页面1
├─ 页面2
└─ 页面3
所有页面
└─ 共享的ViewModel
这很容易导致:
- UI状态混乱混杂
- 意外的跨页面数据共享
- 状态恢复逻辑复杂难维护
- 状态所有权不清晰
使用rememberViewModelStoreProvider的效果
现在每个页面都有独立的生命周期容器:
分页器
├─ 页面1
│└─ ViewModelStoreOwner
│└─ 专属ViewModel
├─ 页面2
│└─ ViewModelStoreOwner
│└─ 专属ViewModel
└─ 页面3
└─ ViewModelStoreOwner
└─ 专属ViewModel
每个页面都拥有完全独立的状态。
适用场景与优势
这个模式特别适合以下场景:
- 标签页UI
- 新手引导流程
- 媒体轮播组件
- 大型懒加载分页器
- 多步骤表单
- 需要独立缓存的内容
现在每个页面都可以:
- ✅ 独立保留自身状态
- ✅ 单独触发资源清理
- ✅ 彻底避免跨页面状态污染
配置变更依然生效
有一个非常重要的细节需要注意:
这些作用域绑定的ViewModel依然支持配置变更(如屏幕旋转)时存活,流程如下:
屏幕旋转
↓
可组合项重建
↓
恢复同一个ViewModel实例
但一旦可组合项被永久移除(比如用户关闭了抽屉、滑走了不再需要的页面),会立即触发清理:
可组合项从UI树中移除
↓
ViewModel被清理
这打造了非常符合直觉的自然生命周期模型。
为什么这符合Compose的设计理念
Jetpack Compose从设计之初就倡导UI驱动的架构。
传统开发模式的逻辑是:
生命周期定义UI
而Compose的逻辑是:
UI定义生命周期
这种逻辑反转的意义非常重大:生命周期现在是组合结构的自然结果,这和Compose的实际运行逻辑高度契合。
最佳实践
临时UI优先使用作用域绑定
以下场景非常适合使用可组合项级作用域:
不要所有场景都默认使用屏幕级的ViewModelOwner。
保持业务域隔离
不要设计什么都管的「上帝ViewModel」:
❌ 一个巨型屏幕级ViewModel
推荐按UI组件拆分作用域:
✅ 屏幕级ViewModel
├─ 工具栏
├─ 分页器
│├─ 页面1 - 专属ViewModel
│└─ 页面2 - 专属ViewModel
├─ 详情区域
└─ 底部抽屉 - 专属ViewModel
更细粒度的生命周期可以大幅减少意外的代码耦合。
避免过度作用域
不要给无状态的小型UI组件创建不必要的ViewModel作用域。一个作用域ViewModel应该对应有实际意义的UI状态所有权。
总结
rememberViewModelStoreOwner() 和 rememberViewModelStoreProvider() 填补了Compose生态长期以来的空白:
可组合项层级的生命周期所有权
这一系列能力可以实现:
- 更精准的内存管理
- 完全隔离的状态容器
- 更整洁的架构设计
- 更合理的分页器状态管理
- 与UI完全对齐的生命周期处理
随着Compose应用的交互越来越复杂动态,这些API很可能会成为未来高级状态管理的基础模式。
拓展阅读
- 别再把ViewModel传给你的可组合项了
- Compose中的ViewModel作用域绑定
- 超越屏幕:Compose中的组件级ViewModel
原文链接:https://proandroiddev.com/stop-building-god-viewmodels-with-one-screen-one-viewmodel-569fe4cb3784