ComposeNavigation嵌套Fragment的兼容性问题之为什么ViewModel不持久了

640 阅读5分钟

问题

我们的项目正在从传统的 Fragment+xml 的形式逐渐迁移至基于纯 Compose 构建的架构中, 路由组件由 navigation 迁移至 compose navigation。

在此背景下由于部分页面没有完全从 Fragment 中剥离,因此使用到了 AndroidFragment 来使 Fragment 嵌入到 Compose 上下文中。

目前面临的其中一个坑是:当页面A(基于Fragment实现) 跳转到页面 B 时,FragmentA 不但自身会被销毁,其所对应的 ViewModelStore 也会被销毁,这就使得当从页面 B 返回 FragmentA 时,FragmentA 和其对应的 ViewModel 都需要重建。对用户来说就是从页面 B 返回,页面 A 会重新刷新。

要知道 ViewModel 设计目的之一就是为了在 Activity/Fragment 临时销毁重建时保持数据,为什么现在失效了呢?

下面是一段简化的使用 ComposeNavigation 构建 NavGraph 的代码

fun NavGraphBuilder.demoNavigationGraph() {
    navigation(
        route = "demo",
        startDestination = "demo/pageA",
    ) {
        composable(
            route = "demo/pageA",
            content = {
                AndroidFragment<PageAFragment>()
            },
        )
        composable(
            route = "demo/pageB",
            content = {
                // Page B
            },
        )
    }
}

PageAFragment.kt

...
import androidx.fragment.app.viewModels
...

class PageAFragment : Fragment() {
    private viewModel: PageAViewModel by viewModels<PageAViewModel>()
    ...
}

经过漫长的 Debug 发现,AndroidFragment 内部使用 DisposableEffect 监听 Compose 组件的销毁,在onDispose中使用FragmentManager 将 Fragment 销毁。并最终在 FragmentManagerViewModel 的 clearNonConfigStateInternal 方法中将 Fragment 对应的 ViewModelStore 也销毁了。

这种做法非常的粗暴,我认为是官方提供的 API 尚不完善。而由于其大量的内部逻辑和类权限限制,我们几乎对此无计可施。

好在此题并非无解,在我重温了一遍 ViewModel 管理的各个 API 后终于有所发现。

探究

Fragment中的ViewModel

首先,我希望先回顾一下 ViewModel 是如何被创建出来的。

当下我们一般都会使用类似于 androidx.fragment.app.viewModels 这样的工具来构建 ViewModel,它的内部会通过 ViewModelStoreOwner 来获取 ViewModelStore 并最终获取或创建所需的 ViewModel。

ViewModelStoreOwner 是一个简单接口,它的实现类通常是 Fragment 或者 Activity。表示可以提供 ViewModelStore 对象。但实际上 ViewModelStore 对象却并不一定是这个实现类自己构建的。

例如 Fragment 的 ViewModelStore 就是通过 FragmentManager 获取的。

下面这段代码是 FragmentManager 中初始化 FragmentManagerViewModel 的代码

void attachController( 
    @NonNull FragmentHostCallback<?> host, 
    @NonNull FragmentContainer container, 
    @Nullable final Fragment parent, 
) {
    // Get the FragmentManagerViewModel 
    if (parent != null) { 
        mNonConfig = parent.mFragmentManager.getChildNonConfig(parent); 
    } else if (host instanceof ViewModelStoreOwner) { 
        ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
        mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore); 
    } else { 
        mNonConfig = new FragmentManagerViewModel(false); 
    }

FragmentManagerViewModel 本身是一个 ViewModel 的字类,并且用它来创建和销毁 ViewModelStore。

上面代码是3种构建 FragmentManagerViewModel 的方式

  1. 通过当前 FragmentManager 的上一级 FragmentManager 来获取或创建, 这样可以使得其创建的 FragmentManagerViewModel 被其父级所管理。
  2. 创建一个新的 FragmentManagerViewModel 并将它添加到从 host 中获取到的 ViewModelStore 中。
  3. 直接创建一个新的 FragmentManagerViewModel

attachController 有两个调用方:

  1. FragmentController 的 attachHost 方法
  2. Fragment 的 performAttach 方法

FragmentController 设计的目的是用来通过其管理 FragmentManager 间接管理 Fragment,看起来似乎只有 FragmentActivity 用到了这个工具。这看起来是一个冗余设计,我猜测可能是历史包袱。

如果忽略那些过度设计,可以归纳如下:

  1. FragmentActivity 通过 FragmentController 管理 FragmentManager
  2. FragmentManager 管理多个 Fragment
  3. Fragment 可以创建一个自己的 FragmentManager(即 mChildFragmentManager)来管理它自己的子 Fragment
  4. FragmentManager 通过 FragmentManagerViewModel 管理 ViewModelStore

基于 Fragment 的路由结构是树状的,根节通常是 FragmentActivity。

            FragmentActivity
                   |
            FragmentController
                   |
            FragmentManager
            |            |
        Fragment      Fragment
         |                 |
    FragmentManager    FragmentManager
     |         |        |          |
Fragment    Fragment  Fragment    Fragment

而迁移到 compose navigation 后,就不再使用这样的路由结构了

Compose中的ViewModel

那么在纯 Compose 环境 ViewModel 是如何创建/获取的呢

观察在 Compose 作用域中使用的类似 androidx.lifecycle.viewmodel.compose.viewModel 这样的工具方法,可以发现它所使用的 ViewModelStoreOwner 是从 LocalViewModelStoreOwner.current 中获取到的。

@Composable
public fun <VM : ViewModel> viewModel(
    modelClass: KClass<VM>,
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    factory: ViewModelProvider.Factory? = null,
    extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
        viewModelStoreOwner.defaultViewModelCreationExtras
    } else {
        CreationExtras.Empty
    }
): VM = viewModelStoreOwner.get(modelClass, key, factory, extras)

LocalXxx 是 Compose 中的环境配置继承树机制,类似于 Flutter 中的 InherentedWidget, 它会寻找距离调用处最近的 Provider 所提供的值

下面这段代码就是提供了 LocalViewModelStoreOwner 的 Provider

@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
    saveableStateHolder: SaveableStateHolder,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalViewModelStoreOwner provides this,
        LocalLifecycleOwner provides this,
        LocalSavedStateRegistryOwner provides this
    ) {
        saveableStateHolder.SaveableStateProvider(content)
    }
}

可以看到 LocalViewModelStoreOwner 的值就是 NavBackStackEntry

NavBackStackEntry 是 compose navigation 中的路由节点, 也是除了 FragmentActivity 和 Fragment 之外另一个 ViewModelStoreOwner 的实现类。

compose navigation 是独立于 Fragment 路由体系的,它们虽然都提供了类似的对于 ViewModel 的处理,但却并不兼容。

解决

因此在迁移到 compose navigation 后,应该从 NavBackStackEntry 中获取 ViewModelStore 进而 创建/获取 ViewModel。以下是一些解决方法:

在 Compose 作用域中使用 androidx.lifecycle.viewmodel.compose.viewModel 等函数获取 ViewModel


如果不方便在 Compose 作用域,也可以继续使用 androidx.fragment.app.viewModels 这样的函数。一般它会提供一个入参让开发者传入一个特定的 ViewModelOwner, 例如下面这个函数签名中的 ownerProducer

@MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline extrasProducer: (() -> CreationExtras)? = null,
    noinline factoryProducer: (() -> Factory)? = null
)

可以通过 NavController 获取 currentBackStackEntry 作为 ViewModelStoreOwner,如下:

class FooFragment : Fragment() {
    private val viewModel: FooViewModel by viewModels<FooViewModel>(
        ownerProducer = {
            val navController = Navigation.findNavController(requireView())
            navController.currentBackStackEntry!!
        }
    )
}

注意 Fragment.findNavController 也存在和 ViewModel 类似的体系隔离问题,获取到的 NavController 与 compose navigation 中的 NavController 不是同一个对象。所以这里要从 view 获取


除此之外,androidx.navigation 库的 NavGraphViewModelLazy.kt 文件中还提供了一些 Fragment 的扩展函数用于构建 ViewModel。

@MainThread
public inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
    navGraphRoute: String,
    noinline extrasProducer: (() -> CreationExtras)? = null,
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
    val backStackEntry by lazy { findNavController().getBackStackEntry(navGraphRoute) }
    val storeProducer: () -> ViewModelStore = { backStackEntry.viewModelStore }
    return createViewModelLazy(
        VM::class,
        storeProducer,
        { extrasProducer?.invoke() ?: backStackEntry.defaultViewModelCreationExtras },
        factoryProducer ?: { backStackEntry.defaultViewModelProviderFactory }
    )
}

它的思路就跟上面提到的解决方案类似,可以获取指定 route 的 NavBackStackEntry。而且通过这样的方式还可以在不同的路由之间实现 ViewModel 的共享。

以上,感谢浏览 🙏