【Navigation3】ViewModel(三)

0 阅读3分钟

背景

教程ViewModel(一)提到过如何在Nav3中使用ViewModel。总结起来就是使用lifecycle-viewmodel-navigation3库中提供的API来注入ViewModel。但是实际会遇到一个问题,就是多页面之间无法共享ViewModel,导致页面间的逻辑、数据无法复用。

共享ViewModel

要解决多页面间的共享ViewModel,实际要解决ViewModelStore共享问题。因为在构造ViewModel时,会将对象临时保存到ViewModelStore上,下一次获取相同的ViewModel时,直接从ViewModelStore上获取缓存,无需构建新对象。

如何共享ViewModelStore,无非就是解决以下问题:

  1. 需要定义一个什么样的数据结构保存,缓存已经构建的ViewModelStore?
  2. 如何在每次在构建ViewModel,可以按需是否从缓存中,获取共享的ViewModelStore?
  3. 页面退出时,是否要销毁当前缓存中的ViewModelStore?

官方解决案例源码,可供参考。源码链接

package com.example.nav3recipes.sharedviewmodel

/*
 * 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.
 */


import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.enableSavedStateHandles
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavEntryDecorator
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner

/**
 * Returns a [SharedViewModelStoreNavEntryDecorator] that is remembered across recompositions.
 *
 * @param [viewModelStoreOwner] The [ViewModelStoreOwner] that provides the [ViewModelStore] to
 *   NavEntries
 * @param [removeViewModelStoreOnPop] A lambda that returns a Boolean for whether the store for a
 *   [NavEntry] should be removed when the [NavEntry] is popped from the backStack. If true, the
 *   entry's ViewModelStore will be removed.
 */
@Composable
fun <T : Any> rememberSharedViewModelStoreNavEntryDecorator(
    viewModelStoreOwner: ViewModelStoreOwner =
        checkNotNull(LocalViewModelStoreOwner.current) {
            "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
        },
    removeViewModelStoreOnPop: () -> Boolean = { true },
): SharedViewModelStoreNavEntryDecorator<T> {
    val currentRemoveViewModelStoreOnPop = rememberUpdatedState(removeViewModelStoreOnPop)
    return remember(viewModelStoreOwner, currentRemoveViewModelStoreOnPop) {
        SharedViewModelStoreNavEntryDecorator(
            viewModelStoreOwner.viewModelStore,
            removeViewModelStoreOnPop,
        )
    }
}

/**
 * Provides the content of a [NavEntry] with a [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 be supplied instead of creating a new one. This allows the
 * entry to access its parent's [ViewModel]s.
 *
 * @see [SharedViewModelStoreNavEntryDecorator.parent]
 *
 * This requires the usage of [androidx.navigation3.runtime.SaveableStateHolderNavEntryDecorator] to
 * ensure that the [NavEntry] scoped [ViewModel]s can properly provide access to
 * [androidx.lifecycle.SavedStateHandle]s
 *
 * @param [viewModelStore] The [ViewModelStore] that provides to NavEntries
 * @param [removeViewModelStoreOnPop] A lambda that returns a Boolean for whether the store for a
 *   [NavEntry] should be cleared when the [NavEntry] is popped from the backStack. If true, the
 *   entry's ViewModelStore will be removed.
 * @see NavEntryDecorator.onPop for more details on when this callback is invoked
 */
class SharedViewModelStoreNavEntryDecorator<T : Any>(
    viewModelStore: ViewModelStore,
    removeViewModelStoreOnPop: () -> Boolean,
) :
    NavEntryDecorator<T>(
        onPop = ({ key ->
            if (removeViewModelStoreOnPop()) {
                viewModelStore.getEntryViewModel().clearViewModelStoreOwnerForKey(key)
            }
        }),
        decorate = { entry ->

            // If the entry indicates it has a parent, use its parent's ViewModelStore.
            val contentKey = entry.metadata[PARENT_CONTENT_KEY] ?: entry.contentKey
            val viewModelStore =
                viewModelStore.getEntryViewModel().viewModelStoreForKey(contentKey)

            val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
            val childViewModelStoreOwner = remember {
                object :
                    ViewModelStoreOwner,
                    SavedStateRegistryOwner by savedStateRegistryOwner,
                    HasDefaultViewModelProviderFactory {
                    override val viewModelStore: ViewModelStore
                        get() = viewModelStore

                    override val defaultViewModelProviderFactory: ViewModelProvider.Factory
                        get() = SavedStateViewModelFactory()

                    override val defaultViewModelCreationExtras: CreationExtras
                        get() =
                            MutableCreationExtras().also {
                                it[SAVED_STATE_REGISTRY_OWNER_KEY] = this
                                it[VIEW_MODEL_STORE_OWNER_KEY] = this
                            }

                    init {
                        require(this.lifecycle.currentState == Lifecycle.State.INITIALIZED) {
                            "The Lifecycle state is already beyond INITIALIZED. The " +
                                    "SharedViewModelStoreNavEntryDecorator requires adding the " +
                                    "SavedStateNavEntryDecorator to ensure support for " +
                                    "SavedStateHandles."
                        }
                        enableSavedStateHandles()
                    }
                }
            }
            CompositionLocalProvider(LocalViewModelStoreOwner provides childViewModelStoreOwner) {
                entry.Content()
            }
        },
    ) {

        companion object {

            private const val PARENT_CONTENT_KEY = "shared_decorator_parent_content_key"

            /**
             * Use this function to specify a `NavEntry`'s parent. The parent's
             * `ViewModelStoreOwner` will be supplied using `LocalViewModelStoreOwner` rather than
             * creating a new `ViewModelStoreOwner` for this `NavEntry`.
             */
            fun parent(contentKey: Any) = mapOf(PARENT_CONTENT_KEY to contentKey)
        }

}

private class EntryViewModel : ViewModel() {
    private val owners = mutableMapOf<Any, ViewModelStore>()

    fun viewModelStoreForKey(key: Any): ViewModelStore = owners.getOrPut(key) { ViewModelStore() }

    fun clearViewModelStoreOwnerForKey(key: Any) {
        owners.remove(key)?.clear()
    }

    override fun onCleared() {
        owners.forEach { (_, store) -> store.clear() }
    }
}


private fun ViewModelStore.getEntryViewModel(): EntryViewModel {
    val provider =
        ViewModelProvider.create(
            store = this,
            factory = viewModelFactory { initializer { EntryViewModel() } },
        )
    return provider[EntryViewModel::class]
}

上面案例可以精细控制ViewModel的生命周期。比如

  • 当前ViewModel是否能被其他页面共享,共享范围以页面单位计算。
  • 若要将ViewModel设置为全局共享页面,只需要将ViewModel注入到首页。后面的子页面,都能全局共享此ViewModel
  • 若要移除ViewModel,只需要将相关联的页面全部关闭即可