androidx.lifecycle 2.11.0-beta01 的发布。不只是普通 bugfix,还调整了 rememberViewModelStoreOwner 和 rememberViewModelStoreProvider。
它解决的是一个老问题:列表项、Pager 页面、局部复杂组件,能不能拥有自己的 ViewModel 生命周期。
以前 ViewModel 主要跟着屏幕走
在 Compose 里直接写:
@Composable
fun FeedScreen(
viewModel: FeedViewModel = viewModel()
) {
// ...
}
这个 ViewModel 默认会绑定到最近的 ViewModelStoreOwner。
通常是 Navigation 的一个 destination,也可能是宿主 Activity。
这很适合“一个页面一个 ViewModel”。问题是,很多 UI 不再只是一个页面。
比如一个信息流列表,每个帖子都有展开状态、点赞中的 loading、评论草稿、图片加载控制、局部上报逻辑。
这些状态全塞进 FeedViewModel,最后会变成一个巨大的 Map<PostId, RowState>。
data class FeedUiState(
val posts: List<Post>,
val expandedPostIds: Set<String>,
val pendingLikeIds: Set<String>,
val commentDrafts: Map<String, String>,
val imageRetryCount: Map<String, Int>,
)
这不是不能写,但复杂度会集中到父 ViewModel。
父层需要理解每个 item 的内部状态,还要处理 item 移除、插入、重排后的清理问题。
key 只能解决实例,不解决清理
很多人会想到 viewModel(key = post.id)。
@Composable
fun PostRow(
post: Post,
viewModel: PostViewModel = viewModel(key = post.id)
) {
// ...
}
这段代码能让不同 post.id 拿到不同 ViewModel 实例,但它们仍然属于同一个父 ViewModelStoreOwner。
如果父 owner 是当前页面,这些 item ViewModel 会跟着页面一起活着。
列表滚出屏幕、数据被分页替换、某条帖子从列表移除,都不一定会触发对应 ViewModel 的清理。
key 解决的是“同一个 owner 下如何区分实例”,不是“这个实例该跟哪块 UI 一起销毁”。
这也是以前 Compose 做 per-item ViewModel 最别扭的地方:可以造实例,但作用域不自然。
现在可以创建局部 owner
Lifecycle 2.11 新增的能力,是在 Compose 层级里创建 ViewModelStoreOwner。
最小用法是:
@Composable
fun ProfileCard() {
val owner = rememberViewModelStoreOwner()
CompositionLocalProvider(LocalViewModelStoreOwner provides owner) {
val viewModel: ProfileCardViewModel = viewModel()
ProfileCardContent(viewModel)
}
}
这个 owner 绑定到当前 composable 调用点。
当这块 UI 永久离开 composition,它关联的 ViewModelStore 会被清理,里面的 ViewModel 会走 onCleared()。
它和普通 remember 的区别在于:ViewModel 仍然能跨配置变更存活。
所以它不是把 ViewModel 降级成普通 Compose state,而是让 ViewModel 有了更细的 UI 作用域。
LazyColumn 的provider
列表项更常见的写法,是先在列表外面创建一个 provider,再用 item 的稳定 key 创建 owner。
@Composable
fun FeedScreen(posts: List<Post>) {
val storeProvider = rememberViewModelStoreProvider()
LazyColumn {
items(
items = posts,
key = { post -> post.id }
) { post ->
val owner = rememberViewModelStoreOwner(
provider = storeProvider,
key = post.id
)
CompositionLocalProvider(LocalViewModelStoreOwner provides owner) {
val viewModel: PostItemViewModel = viewModel()
PostRow(post = post, viewModel = viewModel)
}
}
}
}
这里有两个关键点。
rememberViewModelStoreProvider() 要在列表外面创建。它负责管理多个子 store。
key 要用业务稳定 ID,不要用 index。列表插入一条新数据后,index 会整体偏移,ViewModel 状态会串到别的 item 上。
Pager 页面
HorizontalPager 是这个 API 的典型场景。
每个页面可能都有独立请求、滚动位置、临时输入、播放状态。页面切来切去时,状态要保留;页面被真正移除时,状态要释放。
@Composable
fun ProfilePager(pages: List<ProfilePage>) {
val provider = rememberViewModelStoreProvider()
val pagerState = rememberPagerState(pageCount = { pages.size })
HorizontalPager(state = pagerState) { page ->
val pageData = pages[page]
val owner = rememberViewModelStoreOwner(
provider = provider,
key = pageData.id
)
CompositionLocalProvider(LocalViewModelStoreOwner provides owner) {
val viewModel: ProfilePageViewModel = viewModel()
ProfilePageContent(pageData, viewModel)
}
}
}
相比在父 ViewModel 里维护 Map<PageId, PageState>,这种写法更接近 UI 的真实结构。
页面有页面自己的 owner,页面里的 ViewModel 也只服务这一个页面。
需要注意的是,Pager 的预加载页面也会进入 composition。
如果 ViewModel 初始化就发网络请求,要确认这是不是你想要的行为。否则应该把重活放到明确的事件里,或者让数据层做去重。
Hilt 和 SavedStateHandle
官方文档里提到,子 owner 会继承父作用域里的默认 ViewModelProvider.Factory、CreationExtras、SavedStateRegistryOwner 等信息。
这意味着常见的 Hilt ViewModel、SavedStateHandle、自定义 factory,不需要为每个 item 手写一套工厂。
如果项目里用 Hilt,业务代码仍然保持这个形状:
@HiltViewModel
class PostItemViewModel @Inject constructor(
private val repository: FeedRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
// item 局部状态和业务逻辑
}
Composable 里根据项目依赖选择 viewModel() 或 hiltViewModel()。
更重要的是,不要把 repository、use case 这种长生命周期对象放到 item ViewModel 里重复创建。
ViewModel 可以 per-item,数据层不应该 per-item 复制一份。
不要给所有 item 都上 ViewModel
这个 API 容易被误用。
不是每个列表项都应该有 ViewModel。
如果 item 只是展示标题、头像、价格、开关状态,直接把状态从父层传进去就够了。
@Composable
fun SimplePostRow(
post: Post,
liked: Boolean,
onLikeClick: () -> Unit,
) {
// 纯展示 + 事件上抛
}
更适合 per-item ViewModel 的场景通常有几个特征:
-
• item 内部有独立异步任务
-
• item 状态复杂,父层维护会变成大量 Map
-
• item 会动态加入和移除,清理逻辑很容易漏
-
• Pager 页面、局部编辑器、可折叠复杂卡片需要跨配置变更保留状态
如果只是 expanded、checked 这类轻状态,rememberSaveable(key = post.id) 往往更直接。
var expanded by rememberSaveable(post.id) {
mutableStateOf(false)
}
ViewModel 是生命周期工具,不是所有状态的默认容器。
最后
rememberViewModelStoreOwner 让 Compose 的 ViewModel 作用域终于不再只能绑定到屏幕。
对复杂列表和 Pager 来说,它补上了一个长期缺口:局部 UI 可以有自己的 ViewModel 生命周期,同时仍然保留配置变更能力。
但它不改变基本原则:简单状态留在 Compose,屏幕状态留在 screen ViewModel,只有真正复杂、独立、需要清理的局部 UI,才需要拆成 component-level ViewModel。