androidx.lifecycle 2.11.0-beta01 已經發布。不只是一般的 bug 修正,還調整了 rememberViewModelStoreOwner 和 rememberViewModelStoreProvider。
它要解決的是一個老問題:列表項、Pager 頁面、局部複雜元件,能不能擁有自己的 ViewModel 生命週期。

在 Compose 裡直接寫:
bash 体验AI代码助手 代码解读复制代码@Composable
fun FeedScreen(
viewModel: FeedViewModel = viewModel()
) {
// ...
}
這個 ViewModel 預設會綁定到最近的 ViewModelStoreOwner。
通常是 Navigation 的某個 destination,也可能是宿主 Activity。
這很適合「一個頁面一個 ViewModel」。問題是,很多 UI 已經不再只是單一頁面。
比如一個資訊流列表,每個貼文都有展開狀態、按讚中的 loading、留言草稿、圖片載入控制、局部回報邏輯。
這些狀態全部塞進 FeedViewModel,最後就會變成一個巨大的 Map<PostId, RowState>。
bash 体验AI代码助手 代码解读复制代码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 移除、插入、重排後的清理問題。

很多人會想到 viewModel(key = post.id)。
bash 体验AI代码助手 代码解读复制代码@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 最彆扭的地方:可以建立實例,但作用域不自然。
Lifecycle 2.11 新增的能力,是在 Compose 階層裡建立 ViewModelStoreOwner。
最小用法是:
bash 体验AI代码助手 代码解读复制代码@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 作用域。
列表項更常見的寫法,是先在列表外建立一個 provider,再用 item 的穩定 key 建立 owner。
bash 体验AI代码助手 代码解读复制代码@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 上。

HorizontalPager 是這個 API 的典型場景。
每個頁面可能都有獨立請求、捲動位置、暫時輸入、播放狀態。頁面切換時,狀態要保留;頁面被真正移除時,狀態要釋放。
bash 体验AI代码助手 代码解读复制代码@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 一初始化就發網路請求,要確認這是不是你想要的行為。否則應該把重工作放到明確的事件裡,或是讓資料層做去重。
官方文件提到,子 owner 會繼承父作用域裡的預設 ViewModelProvider.Factory、CreationExtras、SavedStateRegistryOwner 等資訊。
這表示常見的 Hilt ViewModel、SavedStateHandle、自訂 factory,不需要為每個 item 手寫一套工廠。
如果專案有用 Hilt,業務程式碼仍然維持這個樣子:
bash 体验AI代码助手 代码解读复制代码@HiltViewModel
class PostItemViewModel @Inject constructor(
private val repository: FeedRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
// item 局部狀態和業務邏輯
}
Composable 裡根據專案依賴選擇 viewModel() 或 hiltViewModel()。
更重要的是,不要把 repository、use case 這種長生命週期物件放進 item ViewModel 裡重複建立。
ViewModel 可以是 per-item,但資料層不應該每個 item 都複製一份。

這個 API 很容易被誤用。
不是每個列表項都應該有 ViewModel。
如果 item 只是顯示標題、頭像、價格、開關狀態,直接把狀態從父層傳進去就夠了。
bash 体验AI代码助手 代码解读复制代码@Composable
fun SimplePostRow(
post: Post,
liked: Boolean,
onLikeClick: () -> Unit,
) {
// 純展示 + 事件上拋
}
比較適合 per-item ViewModel 的場景通常有幾個特徵:
如果只是 expanded、checked 這類輕量狀態,rememberSaveable(key = post.id) 往往更直接。
bash 体验AI代码助手 代码解读复制代码var expanded by rememberSaveable(post.id) {
mutableStateOf(false)
}
ViewModel 是生命週期工具,不是所有狀態的預設容器。
rememberViewModelStoreOwner 讓 Compose 的 ViewModel 作用域終於不再只能綁定到畫面。
對複雜列表和 Pager 來說,它補上了一個長期缺口:局部 UI 可以有自己的 ViewModel 生命週期,同時仍然保留配置變更能力。
但它不會改變基本原則:簡單狀態留在 Compose,畫面狀態留在 screen ViewModel,只有真正複雜、獨立、需要清理的局部 UI,才需要拆成 component-level ViewModel。
#Android #JetpackCompose #ViewModel #AndroidX