21.2 Compose Navigation + BottomBar 切换页面状态丢失问题

1,773 阅读2分钟

项目中首页底部导航 Navigation + BottomBar 按 官方文档 中配置,实现第二页面时发现了问题

底部导航切换时只能保存第一个页面的状态

Untitled.gif

首页从发现页切换回来列表仍显示切换前的位置,再切换到发现页没有保存上次的状态。

问题出现的原因是底部导航按钮 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!!)
            }
         }
        }

Untitled.gif