上一篇文章 ComposeNavigation嵌套Fragment的兼容性问题之为什么ViewModel不持久了 讲了从 navigation 迁移至 compose navigation 的其中一个坑, 本篇是另一个...
问题
下面是一个简化过的示例。在一个 compose navigation 架构的 NavGraph 中通过 AndroidFragment
嵌入一个 Fragment 路由节点。这个 Fragment 的 UI 部分是 Compose 的,因此用到了 ComposeView
在这种情况下,用于记录列表滚动位置的 listState
或者其他基于 rememberSavable
的 API 都失去了状态持久化的能力。
fun NavGraphBuilder.demoNavigationGraph() {
navigation(...) {
composable(
route = "demo/fooScreen",
content = {
AndroidFragment<FooFragment>()
},
)
...
}
}
class FooFragment : Fragment() {
override fun onCreateView(...): View = ComposeView(requireContext()).apply {
setContent {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
...
}
}
}
直接上答案
使用 androidx.fragment.compose 包下的 fun Fragment.content(content: @Composable () -> Unit): ComposeView
方法替换 ComposeView(requireContext())
override fun onCreateView(...): View = content {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
...
}
}
如果没有找到这个扩展方法,请尝试升级 compose 版本(我的是 1.8.5)
如果不方便升级, 可以将 ViewCompositionStrategy 设置为 DisposeOnViewTreeLifecycleDestroyed
, 这正是官方的扩展方法所做的
override fun onCreateView(...): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
...
}
}
}
探究
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed 的说明:
ViewCompositionStrategy that disposes the composition when the LifecycleOwner returned by findViewTreeLifecycleOwner of the next window the view is attached to is destroyed. This strategy is appropriate for Compose UI views that share a 1-1 relationship with their closest LifecycleOwner, such as a Fragment view.
简而言之 Compose 的销毁会绑定到 Fragment 的 mViewLifecycleOwner
的 ON_DESTORY 事件上。
而默认策略 DisposeOnDetachedFromWindowOrReleasedFromPool
则会导致 Compose 在更早的时机销毁。(更深入的对 ViewCompositionStrategy 探究请搜索其他文章)
答案很简单,但探寻答案的过程却挺曲折。来看看状态保存机制是如何运作的, Compose 的销毁的时机和状态保存有什么样的关系呢?
Fragment 的状态保存和 Compose 的状态保存有什么联系?
AndroidFragment 内部的 DisposableEffect
在 onDispose
触发时会保存 Fragment 的状态,以便在 Fragment 重建时还原状态。以下代码节自 AndroidFragment,只保留关键部分。
@Composable
fun <T : Fragment> AndroidFragment(
fragmentState: FragmentState = rememberFragmentState(),
) {
// fragmentState 也是 rememberSaveble 方法,但它由其父级组件的状态管理,因此不在本文问题中。
DisposableEffect(fragmentManager, containerFactory, clazz, fragmentState) {
val fragment = fragmentManager.fragmentFactory.apply {
// 在 fragment 重建时还原保存的状态
setInitialSavedState(fragmentState.state.value)
...
}
...
onDispose {
// 保存 Fragment 状态
val state = fragmentManager.saveFragmentInstanceState(fragment)
// 将 Fragment 状态存到 fragmentState
fragmentState.state.value = state
...
}
}
}
由 Fragment 发起的状态保存事件如何传导到 Compose 的各个组件里呢?
Fragment 和 Compose 是两套体系,Fragment 的状态保存并不能直接作用到 Compose 上,关键在于二者的连接。这涉及到两个关键的API SavedStateRegistry
和 SaveableStateRegistry
。乍一看有点像哈,我做了加粗标识
- SavedStateRegistry 是 Fragment 体系下用于保存状态的工具
- SaveableStateRegistry 是 Compose 体系下用于保存状态的工具
Registry 本身并不负责具体的保存/还原操作,而是管理分发保存/还原的事件。
在 Compose 中, rememberSavable 会通过 LocalSaveableStateRegistry 查找距离最近的 SaveableStateRegistry, 并将自身注册到 SaveableStateRegistry 中。当保存/还原事件发生时, rememberSavable 中的 Saver 会被触发,将其持有的状态保存到 bundle 中或从 bundle 中还原上次的状态。
SavedStateRegistry 如何桥接到 SaveableStateRegistry
二者的连接在于,SavedStateRegistry 提供了 registerSavedStateProvider
方法用于注册 SavedStateProvider
。当需要保存状态时, 会通知 SavedStateProvider 进行保存操作。
有了这样的监听机制, Compose 的 API 就可以注册一个自己的 SavedStateProvider,并将保存还原等事件桥接到 SaveableStateRegistry 中。这可以在 ComposeView 的构建过程中可以看到
在 ComposeView 的构建过程中,经过层层的封装,最终会构建一个 DisposableSaveableStateRegistry
的对象,它是 SaveableStateRegistry 的实现类之一,但只有这个实现类是直接用于跟 SavedStateRegistry 体系桥接。(如果想深入了解,建议先定位到 DisposableSaveableStateRegistry,再层层向上检索,这样比较方便查看它的调用路径。)
DisposableSaveableStateRegistry 的入参之一是 SavedStateRegistryOwner
,有了它才能获取到 SavedStateRegistry,继而注册 SavedStateProvider 实现两种体系的桥接。 那么这个入参是怎么来的呢?
这需要往回倒一步,它来源于 AndroidComposeView.viewTreeOwners
, 主要代码如下
override fun onAttachedToWindow() {
...
val lifecycleOwner = findViewTreeLifecycleOwner()
val savedStateRegistryOwner = findViewTreeSavedStateRegistryOwner()
...
if (resetViewTreeOwner) {
...
val viewTreeOwners = ViewTreeOwners(
lifecycleOwner = lifecycleOwner,
savedStateRegistryOwner = savedStateRegistryOwner
)
_viewTreeOwners = viewTreeOwners
...
}
...
}
AndroidComposeView 也是在 ComposeView 的构建过程中创建的。它是 View 体系和 Compose 体系桥接的具体实现,本文无需关注。
继续深入会发现 lifecycleOwner 和 savedStateRegistryOwner 都来源于 Fragment
既然搭上线了为什么 rememberSavable 还是不好使了呢?
我 debug 了很久,其实原因很简单:Compose 先于 Fragment 的状态保存前就销毁了,等 Fragment 触发保存的时候,rememberSavable 早就 unregister 了。
现在回到 ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,它的作用就是延后了 Compose 的存活时间。会先执行保存,才销毁 Compose。
以上,感谢浏览🙏