共享 ViewModel 示例
此方案演示了如何使用自定义 NavEntryDecorator 在 Navigation 3 中不同屏幕(条目)之间共享 ViewModel。
工作原理
此示例定义了三个界面:
ParentScreen:显示一个用于递增计数器的按钮,计数器状态保存在CounterViewModel中。ChildScreen:一个可以更新ParentScreen的计数器状态以及自身隔离状态的子界面。StandaloneScreen:一个独立的屏幕,仅具有自己的隔离状态。
SharedViewModelStoreNavEntryDecorator
此配方的核心是 SharedViewModelStoreNavEntryDecorator。此装饰器用于管理导航条目的 ViewModelStore。它允许条目指定一个可选的“父”条目,并共享该条目的 ViewModelStore。
在 SharedViewModelActivity.kt 中,NavDisplay 通过此装饰器进行配置:
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberSharedViewModelStoreNavEntryDecorator(),
)
共享 ViewModel
为了启用共享,ChildScreen 条目使用元数据明确定义了其父条目:
entry<ChildScreen>(
metadata = SharedViewModelStoreNavEntryDecorator.parent(
ParentScreen.toContentKey()
)
) {
// ...
}
toContentKey() 扩展函数用于标准化父级 NavEntry 的 contentKey 的指定方式,无论是在定义父级时还是在子级通过元数据引用父级时,都使用此函数。
当 ChildScreen 请求父级 CounterViewModel 时:
val parentViewModel = viewModel(
modelClass = CounterViewModel::class,
viewModelStoreOwner = LocalSharedViewModelStoreOwner.current
)
该装饰器可确保它接收到 ParentScreen 正在使用的同一实例,因为它使用的是 ParentScreen 的 ViewModelStore。
ChildScreen 仍可从默认 LocalViewModelStoreOwner 请求自己的 CounterViewModel:
val standaloneViewModel = viewModel(
modelClass = CounterViewModel::class,
)
相比之下,StandaloneScreen 未定义父级,因此它仅获取自己的全新 ViewModelStore 和 CounterViewModel 的新实例。
/* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.sharedviewmodel import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavEntryDecorator import androidx.navigation3.runtime.NavMetadataKey import androidx.navigation3.runtime.get import androidx.navigation3.runtime.metadata import androidx.savedstate.compose.LocalSavedStateRegistryOwner import androidx.lifecycle.viewmodel.ViewModelStoreProvider import androidx.lifecycle.viewmodel.compose.rememberViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.rememberViewModelStoreProvider import androidx.navigation3.runtime.SaveableStateHolderNavEntryDecorator import kotlin.collections.mutableListOf /** * Returns a [SharedViewModelStoreNavEntryDecorator] that is remembered across recompositions. * * @param [viewModelStoreOwner] The [ViewModelStoreOwner] that provides the [ViewModelStore] to * NavEntries */ @Composable fun <T : Any> rememberSharedViewModelStoreNavEntryDecorator( viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" }, ): SharedViewModelStoreNavEntryDecorator<T> { val viewModelStoreProvider = rememberViewModelStoreProvider(viewModelStoreOwner) return remember(viewModelStoreOwner) { SharedViewModelStoreNavEntryDecorator( viewModelStoreProvider, ) } } /** * Provides the content of a [NavEntry] with a new [ViewModelStoreOwner] and provides that * [ViewModelStoreOwner] as a [LocalViewModelStoreOwner] so that it is available within the content. * * If the [NavEntry] specifies that it has a parent in its metadata, the parent's * [ViewModelStoreOwner] will also be supplied along with the new one. This allows the * entry to access both its own [ViewModel] and its parent's [ViewModel]s. * * This requires the usage of [SaveableStateHolderNavEntryDecorator] to ensure that the [NavEntry] * scoped [ViewModel]s can properly provide access to [androidx.lifecycle.SavedStateHandle]s. * * @see [SharedViewModelStoreNavEntryDecorator.parent] * * @param [viewModelStoreProvider] The [ViewModelStoreProvider] scoped to * the parent [ViewModelStoreOwner] */ class SharedViewModelStoreNavEntryDecorator<T : Any>( viewModelStoreProvider: ViewModelStoreProvider ) : NavEntryDecorator<T>( onPop = { key -> viewModelStoreProvider.clearKey(key) }, decorate = { entry -> val localContentKey = entry.contentKey val localOwner = rememberViewModelStoreOwner( viewModelStoreProvider, localContentKey, savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, ) val localValues: MutableList<ProvidedValue<*>> = mutableListOf(LocalViewModelStoreOwner provides localOwner) // If the entry indicates it has a parent, also provide its parent's ViewModelStore val parentContentKey = entry.metadata[ParentKey] if (parentContentKey != null) { val parentOwner = rememberViewModelStoreOwner( viewModelStoreProvider, parentContentKey, savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, ) localValues.add(LocalSharedViewModelStoreOwner provides parentOwner) } CompositionLocalProvider( values = localValues.toTypedArray() ) { entry.Content() } }, ) { companion object { /** * Use this function to specify a `NavEntry`'s parent. The parent's * `ViewModelStoreOwner` will be supplied via `LocalSharedViewModelStoreOwner` */ fun parent(key: Any) = metadata { put(ParentKey, key) } object ParentKey : NavMetadataKey<Any> } } val LocalSharedViewModelStoreOwner = staticCompositionLocalOf<ViewModelStoreOwner> { error("No LocalSharedViewModelStoreOwner provided!") }
/* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.sharedviewmodel import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.content.ContentBlue import com.example.nav3recipes.content.ContentGreen import com.example.nav3recipes.content.ContentRed import com.example.nav3recipes.ui.setEdgeToEdgeConfig import kotlinx.serialization.Serializable @Serializable private data object ParentScreen : NavKey @Serializable private data object ChildScreen : NavKey @Serializable private data object StandaloneScreen : NavKey class SharedViewModelActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { val backStack = rememberNavBackStack(ParentScreen) NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberSharedViewModelStoreNavEntryDecorator(), ), entryProvider = entryProvider { entry<ParentScreen>( clazzContentKey = { key -> key.toContentKey() }, ) { val viewModel = viewModel<CounterViewModel>() ContentRed("Parent screen") { Button(onClick = dropUnlessResumed { viewModel.count++ }) { Text("Count: ${viewModel.count}") } Button(onClick = dropUnlessResumed { backStack.add(ChildScreen) }) { Text("View child screen") } } } entry<ChildScreen>( metadata = SharedViewModelStoreNavEntryDecorator.parent( ParentScreen.toContentKey() ) ) { val parentViewModel = viewModel<CounterViewModel>( viewModelStoreOwner = LocalSharedViewModelStoreOwner.current ) val standaloneViewModel = viewModel<CounterViewModel>() ContentBlue("Child screen") { Button(onClick = dropUnlessResumed { parentViewModel.count++ }) { Text("Parent count: ${parentViewModel.count}") } Button(onClick = dropUnlessResumed { standaloneViewModel.count++ }) { Text("Standalone Count: ${standaloneViewModel.count}") } Button(onClick = dropUnlessResumed { backStack.add(StandaloneScreen) }) { Text("View standalone screen") } } } entry<StandaloneScreen> { val viewModel = viewModel<CounterViewModel>() ContentGreen("Standalone screen") { Button(onClick = dropUnlessResumed { viewModel.count++ }) { Text("Count: ${viewModel.count}") } } } } ) } } } fun NavKey.toContentKey() = this.toString() class CounterViewModel : ViewModel() { var count by mutableIntStateOf(0) }