دستور پخت ViewModel مشترک

این دستورالعمل نحوه اشتراک‌گذاری یک ViewModel بین صفحات (ورودی‌های) مختلف در Navigation 3 را با استفاده از یک NavEntryDecorator سفارشی نشان می‌دهد.

چگونه کار می‌کند؟

این مثال سه صفحه نمایش را تعریف می‌کند:

  • 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() برای استانداردسازی نحوه‌ی تعیین contentKey مربوط به کلاس والد NavEntry ، هم هنگام تعریف کلاس والد و هم هنگام ارجاع به فراداده توسط کلاس فرزند، استفاده می‌شود.

وقتی ChildScreen یک مدل والد CounterViewModel درخواست می‌کند:

val parentViewModel = viewModel<CounterViewModel>(
    viewModelStoreOwner = LocalSharedViewModelStoreOwner.current
)

این دکوراتور تضمین می‌کند که همان نمونه‌ای را که 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)
}