解决ListDetailPaneScaffold导航状态保持问题
2025-03-21 19:14:41
ListDetailPaneScaffold 导航状态保持问题及解决方案
使用 ListDetailPaneScaffold
时,经常会遇到一个问题:当 detail pane (详情窗格) 处于展开状态,跳转到另一个界面再返回后,detail pane 变成了隐藏状态。 这篇博客文章将深入分析这个问题的原因,并提供几种可行的解决方案。
一、 问题
在使用 ListDetailPaneScaffold
构建的列表-详情布局中, 如果在详情窗格展开(navigator.scaffoldValue.primary
为 expanded 状态)时导航到另一个界面,然后返回到 ListDetailPaneScaffold
界面,原本展开的详情窗格会变成隐藏状态(navigator.scaffoldValue.primary
会变为 hidden)。
核心问题是,rememberListDetailPaneScaffoldNavigator
并没有像预期那样,在界面重建时自动恢复之前的导航状态。即便它内部使用了 rememberSaveable
,理应可以保存和恢复状态。
下面是重现问题的关键信息:
- 初始状态(选中列表项后):
navigator.currentDestination.content: 6 navigator.currentDestination.pane: Primary PaneAdaptedValue(description=Expanded)
- 跳转到其他界面再返回后:
navigator.currentDestination.content: null navigator.currentDestination.pane: Secondary PaneAdaptedValue(description=Hidden)
二、问题原因分析
rememberListDetailPaneScaffoldNavigator
内部使用 rememberSaveable
来尝试保存状态。正常情况下,rememberSaveable
应该在 Activity 或 Fragment 重建时(例如配置变更, 旋转屏幕), 或者进程被杀死后恢复时,自动恢复之前的状态. 但这里它并没有按照预期工作. 根本原因有几个:
-
跨导航保存 :
rememberSaveable
的设计初衷是用于单个 Composable 的状态保存,主要针对配置更改(例如屏幕旋转),而非跨导航的状态保持。 -
BackStackEntry
生命周期: 当导航到另一个界面时,ListDetailPaneScaffold
所在的BackStackEntry
可能仍然存在于返回栈中,但也可能会因为内存压力被回收,从而导致rememberSaveable
保存的状态丢失. -
currentDestination
重置:rememberListDetailPaneScaffoldNavigator
的实现里可能存在缺陷,返回的时候,它会基于某种默认逻辑设置currentDestination
, 从而覆盖了原本应该通过rememberSaveable
恢复的状态.
三、 解决方案
针对上述问题,提供以下几种解决方案:
1. 手动保存和恢复 PaneScaffoldValue
最直接的办法是,不依赖 rememberListDetailPaneScaffoldNavigator
内部的保存机制,手动管理 ListDetailPaneScaffold
的 scaffoldValue
。
原理:
- 使用
rememberSaveable
显式地保存ListDetailPaneScaffold
的当前PaneScaffoldValue
(即是Expanded
、Hidden
还是其他)。 - 在 Composable 重新创建时,从保存的状态中恢复
PaneScaffoldValue
,并传递给ListDetailPaneScaffold
。
代码示例:
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.navigation.compose.rememberNavController
import androidx.window.layout.DisplayFeature
import com.google.accompanist.adaptive.ListDetailPaneScaffold
import com.google.accompanist.adaptive.PaneScaffoldValue
import com.google.accompanist.adaptive.calculateListDetailPaneScaffoldState
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun MyListDetailScreen() {
var selectedItem: Int? by rememberSaveable { mutableStateOf(null) }
val windowSizeClass = calculateWindowSizeClass()
val displayFeatures = emptyList<DisplayFeature>() //可以根据实际情况添加
val savedScaffoldValue = rememberSaveable {
mutableStateOf(
if (selectedItem != null && windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded)
PaneScaffoldValue.Primary
else
PaneScaffoldValue.Secondary //初始为Secondary, 根据需要修改
)
}
val scaffoldState = calculateListDetailPaneScaffoldState(
scaffoldValue = savedScaffoldValue.value,
windowSizeClass = windowSizeClass,
displayFeatures = displayFeatures
)
ListDetailPaneScaffold(
scaffoldState = scaffoldState,
// ...其他 ListDetailPaneScaffold 的配置...
listPane = {
//列表
MyList(onItemSelected = { item ->
selectedItem = item;
savedScaffoldValue.value = PaneScaffoldValue.Primary
})
},
detailPane = {
selectedItem?.let {
MyDetail(item = it) //详情页,传入选中的item
}
}
)
//如果需要对savedScaffoldValue进行更复杂控制(例如:导航到另一页),可以单独监听savedScaffoldValue.value.
}
@Composable
fun MyList(onItemSelected:(Int)->Unit)
{
//具体列表实现, 当点击时调用 onItemSelected
}
@Composable
fun MyDetail(item:Int){
//具体详情页实现.
}
安全建议:
- 此方法需要手动处理
PaneScaffoldValue
的所有可能情况。 - 应仔细考虑不同窗口大小下的初始状态,以及用户交互后的状态变化。
2. 使用 ViewModel 保存状态
另一种方案是使用 ViewModel 来保存状态,这样可以更可靠地跨越配置更改和导航。
原理:
- 在 ViewModel 中维护选中的条目和
PaneScaffoldValue
。 ListDetailPaneScaffold
从 ViewModel 中读取这些状态。- 由于 ViewModel 的生命周期独立于 Composable,因此即使 Composable 被销毁和重新创建,状态也会保留。
代码示例:
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.window.layout.DisplayFeature
import com.google.accompanist.adaptive.ListDetailPaneScaffold
import com.google.accompanist.adaptive.PaneScaffoldValue
import com.google.accompanist.adaptive.calculateListDetailPaneScaffoldState
class MyViewModel : ViewModel() {
var selectedItem: Int? by mutableStateOf(null)
var scaffoldValue: PaneScaffoldValue by mutableStateOf(PaneScaffoldValue.Secondary) //初始化为Secondary
}
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun MyListDetailScreen(viewModel: MyViewModel) {
val windowSizeClass = calculateWindowSizeClass()
val displayFeatures = emptyList<DisplayFeature>() //可以根据需要修改
val scaffoldState = calculateListDetailPaneScaffoldState(
scaffoldValue = viewModel.scaffoldValue,
windowSizeClass = windowSizeClass,
displayFeatures = displayFeatures
)
ListDetailPaneScaffold(
scaffoldState = scaffoldState,
// ...其他 ListDetailPaneScaffold 的配置...
listPane = {
//列表
MyList(onItemSelected = {item ->
viewModel.selectedItem = item;
viewModel.scaffoldValue = PaneScaffoldValue.Primary
})
},
detailPane = {
viewModel.selectedItem?.let{
MyDetail(item = it)
}
}
)
}
@Composable
fun MyList(onItemSelected:(Int)->Unit)
{
//具体列表实现, 当点击时调用 onItemSelected
}
@Composable
fun MyDetail(item:Int){
//具体详情页实现.
}
安全建议:
- 确保在 ViewModel 中正确处理状态更新, 特别是当多个 Composable 可以修改状态时。
3. 进阶技巧:结合自定义 Saver
(较复杂)
如果需要对保存和恢复过程进行更精细的控制,可以为 rememberSaveable
创建自定义的 Saver
。
原理:
- 创建一个自定义
Saver
,用于明确定义如何保存和恢复PaneScaffoldValue
和当前选中内容. - 将自定义
Saver
与rememberSaveable
一起使用。
注意 此方法涉及较底层的API, 相对复杂, 不做为首选方案.
示例代码: (仅做演示,实际可能更复杂)
import androidx.compose.runtime.saveable.Saver
import com.google.accompanist.adaptive.PaneScaffoldValue
data class ScaffoldStateData(
val scaffoldValue: PaneScaffoldValue,
val selectedItem:Int?
)
object ScaffoldStateSaver : Saver<ScaffoldStateData, Map<String, Any?>> {
private const val SCAFFOLD_VALUE_KEY = "scaffoldValue"
private const val SELECTED_ITEM_KEY = "selectedItem"
override fun restore(value: Map<String, Any?>): ScaffoldStateData? {
val scaffoldValueRaw = value[SCAFFOLD_VALUE_KEY] as? Int ?: return null
val selectedItem = value[SELECTED_ITEM_KEY] as? Int
val scaffoldValue = when (scaffoldValueRaw) {
0-> PaneScaffoldValue.Secondary
1 -> PaneScaffoldValue.Primary
//可以扩展其他状态
else -> PaneScaffoldValue.Secondary //默认
}
return ScaffoldStateData(scaffoldValue, selectedItem)
}
override fun SaverScope.save(value: ScaffoldStateData): Map<String, Any?> {
val scaffoldValueInt = when (value.scaffoldValue){
PaneScaffoldValue.Secondary -> 0
PaneScaffoldValue.Primary -> 1
//可以扩展其他
else -> 0
}
return mapOf(
SCAFFOLD_VALUE_KEY to scaffoldValueInt,
SELECTED_ITEM_KEY to value.selectedItem
)
}
}
//用法示例:
val scaffoldData = rememberSaveable(saver = ScaffoldStateSaver) {
mutableStateOf(ScaffoldStateData(PaneScaffoldValue.Secondary,null))
}
总结:
以上三种方案, 可以按需采用, 通常第一和第二种就能满足大部分的需求. 手动处理和ViewModel提供了比较可靠且方便的方式来保存状态, 适合大多数场景。