项目中首页底部导航 Navigation + BottomBar 按 官方文档 中配置,实现第二页面时发现了问题
底部导航切换时只能保存第一个页面的状态
首页从发现页切换回来列表仍显示切换前的位置,再切换到发现页没有保存上次的状态。
问题出现的原因是底部导航按钮 onClick 事件中 navigate() 方法 NavOptions 参数使用 popUpTo 对 BackStack 进行了管理。
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
popUpTo 会在 navigate() 之前先弹出 BackStack 中的 NavBackStackEntry ,直到参数指定的 NavBackStackEntry 处于栈顶。
假设有 3 个底部导航 A B C ,
默认的 StartDestination 是 A ,BackStack [ A ]
点击 B ,popUpTo(StartDestination.id) BackStack [ A ] ,navigation(B) BackStack [ A , B ]
再点击 C,popUpTo(StartDestination.id) BackStack [ A ] , navigation(C) BackStack [ A , C ]
在点击 A,popUpTo(StartDestination.id) BackStack [ A ] ,navigation(A) launchSingleTop = true BackStack [ A ]
如果不配置 popUpTo 三次点击之后 BackStack[A , B , C , A]
popUpTo 防止了点击时在 BackStack 中添加过多的 NavBackStackEntry ,随之而来的问题是除了 StartDestination 其他导航页面的状态无法保存。
hiltViewModel() 创建的 VM 与 NavBackStackEntry 关联在一起,每次导航前其他页面的 NavBackStackEntry 出栈 ,对应 VM 就会随之失效。
Compose Navigation 中没有 singleInstance 配置,保存其他页面的状态就需要将需要保存的状态保存到 VM 中,再提升 VM 的声明周期。
@HiltViewModel
class DiscoveryViewModel @Inject constructor(
private val wendaPagingInteractor: WendaPagingInteractor,
private val squarePagingInteractor: SquarePagingInteractor
):ComposeViewModel(){
init {
wendaPagingInteractor(WendaPagingInteractor.Params(PAGING_CONFIG))
squarePagingInteractor(SquarePagingInteractor.Params(PAGING_CONFIG))
Log.e("DiscoveryViewModel", "DiscoveryViewModel: $this", )
}
//选项卡选中 index
val selectedTabIndexState = mutableStateOf(0)
//两个列表 LazyListState 的初始信息
var wendaFirstItemInfo = LazyListFirstItemInfo()
var squareFirstItemInfo = LazyListFirstItemInfo()
val wendaPagingDataFlow : Flow<PagingData<Article>> = wendaPagingInteractor.flow.cachedIn(viewModelScope)
val squarePagingDataFlow : Flow<PagingData<Article>> = squarePagingInteractor.flow.cachedIn(viewModelScope)
}
State 实现 RememberObserver 接口 onForgotten() 时 保存列表滚动状态
class UiDiscoveryState(
val selectedTabIndex: State<Int>,
private val pagedWenda: LazyPagingItems<Article>,
private val pagedSquare: LazyPagingItems<Article>,
private val wendaListState: LazyListState,
private val squareListState: LazyListState,
override val navController: NavController,
override val viewModel: DiscoveryViewModel,
override val resources: Resources,
override val coroutineScope: CoroutineScope,
override val isLoading: Boolean
) : ComposeVmState<DiscoveryAction, DiscoveryViewModel>(),RememberObserver {
val currentPagingItems
get() = if (selectedTabIndex.value == 0)pagedSquare else pagedWenda
val currentListState
get() = if (selectedTabIndex.value == 0) squareListState else wendaListState
override fun dispatchAction(action: DiscoveryAction) {
when(action){
is DiscoveryAction.SelectTab -> selectTable(action.index)
DiscoveryAction.RefreshList -> currentPagingItems.refresh()
}
wendaListState.firstVisibleItemScrollOffset
}
private fun selectTable(tabIndex: Int) {
Log.e(TAG, "selectTable: $tabIndex" )
viewModel.selectedTabIndexState.value = tabIndex
}
override fun onAbandoned() {
}
override fun onForgotten() {
viewModel.wendaFirstItemInfo = LazyListFirstItemInfo(wendaListState.firstVisibleItemIndex,wendaListState.firstVisibleItemScrollOffset)
viewModel.squareFirstItemInfo = LazyListFirstItemInfo(squareListState.firstVisibleItemIndex,squareListState.firstVisibleItemScrollOffset)
}
override fun onRemembered() {
}
}
@Composable
fun rememberUiDiscoveryState(
navController: NavController,
viewModel: DiscoveryViewModel
): UiDiscoveryState {
val selectedTabIndex = viewModel.selectedTabIndexState
val pagedWenda = viewModel.wendaPagingDataFlow.collectAsLazyPagingItems()
val pagedSquare = viewModel.squarePagingDataFlow.collectAsLazyPagingItems()
val wendaListState = rememberLazyListState(viewModel.wendaFirstItemInfo.index,viewModel.wendaFirstItemInfo.scrollOffset)
val squareListState = rememberLazyListState(viewModel.squareFirstItemInfo.index,viewModel.squareFirstItemInfo.scrollOffset)
val (isLoading, resources, coroutineScope) = commonState(vm = viewModel)
return remember(viewModel) {
UiDiscoveryState(
selectedTabIndex = selectedTabIndex,
pagedWenda = pagedWenda,
pagedSquare = pagedSquare,
wendaListState = wendaListState,
squareListState = squareListState,
navController = navController,
viewModel = viewModel,
resources = resources,
coroutineScope = coroutineScope,
isLoading = isLoading
)
}
}
data class LazyListFirstItemInfo(
val index:Int = 0,
val scrollOffset: Int = 0,
)
将保存 VM ,将它的生命周期提升到与 WanNavHost 一致
abstract class NavGraph(navController: NavController):ScreenNavGraph(navController,Index){
private var vm: DiscoveryViewModel? = null
override val composeScreens: NavGraphBuilder.() -> Unit = {
composableScreen(Index){
if (vm == null){
vm = hiltViewModel()
}
UiDiscovery(this@NavGraph.navController,vm!!)
}
}
}