สูตร ViewModel ที่แชร์
สูตรนี้แสดงวิธีแชร์ ViewModel ระหว่างหน้าจอ (รายการ) ต่างๆ ใน Navigation 3 โดยใช้ NavEntryDecorator ที่กำหนดเอง
วิธีการทำงาน
ตัวอย่างนี้กำหนดหน้าจอ 3 หน้าจอ
ParentScreen: แสดงปุ่มที่เพิ่มตัวนับ โดยสถานะตัวนับจะอยู่ในCounterViewModelChildScreen: หน้าจอย่อยที่อัปเดตสถานะตัวนับของParentScreenรวมถึงสถานะที่แยกกันของตัวเองได้StandaloneScreen: หน้าจออิสระที่มีเพียงสถานะแยกของตัวเอง
SharedViewModelStoreNavEntryDecorator
หัวใจสำคัญของสูตรนี้คือ SharedViewModelStoreNavEntryDecorator Decorator นี้จัดการ ViewModelStore สำหรับรายการการนำทาง ซึ่งช่วยให้รายการระบุรายการ "หลัก" ที่ไม่บังคับซึ่งควรแชร์ ViewModelStore ได้
ใน SharedViewModelActivity.kt NavDisplay จะได้รับการกำหนดค่าด้วยตัวตกแต่งนี้
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberSharedViewModelStoreNavEntryDecorator(),
)
การแชร์ ViewModel
หากต้องการเปิดใช้การแชร์ รายการ ChildScreen จะกำหนดรายการหลักอย่างชัดเจนโดยใช้ข้อมูลเมตา ดังนี้
entry<ChildScreen>(
metadata = SharedViewModelStoreNavEntryDecorator.parent(
ParentScreen.toContentKey()
)
) {
// ...
}
ฟังก์ชันส่วนขยาย toContentKey() ใช้เพื่อกำหนดมาตรฐานวิธีระบุ contentKey ของ NavEntry หลัก ทั้งเมื่อกำหนดหลักและเมื่ออ้างอิงในข้อมูลเมตาโดยรายการย่อย
เมื่อChildScreenขออนุญาตผู้ปกครองCounterViewModel
val parentViewModel = viewModel<CounterViewModel>(
viewModelStoreOwner = LocalSharedViewModelStoreOwner.current
)
Decorator จะช่วยให้มั่นใจได้ว่าจะได้รับอินสแตนซ์เดียวกันกับที่ ParentScreen ใช้ เนื่องจากใช้ ViewModelStore ของ ParentScreen
ChildScreen ยังคงขอ CounterViewModel ของตัวเองจาก LocalViewModelStoreOwner เริ่มต้นได้โดยทำดังนี้
val standaloneViewModel = viewModel<CounterViewModel>()
ในทางตรงกันข้าม 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.ViewModelStoreProvider import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.rememberViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.rememberViewModelStoreProvider import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavEntryDecorator import androidx.navigation3.runtime.NavMetadataKey import androidx.navigation3.runtime.SaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.get import androidx.navigation3.runtime.metadata import androidx.savedstate.compose.LocalSavedStateRegistryOwner /** * 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 = { 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 = { parentViewModel.count++ }) { Text("Parent count: ${parentViewModel.count}") } Button(onClick = { 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 = { viewModel.count++ }) { Text("Count: ${viewModel.count}") } } } } ) } } } fun NavKey.toContentKey() = this.toString() class CounterViewModel : ViewModel() { var count by mutableIntStateOf(0) }