Best practice to remembering List State after navigation to another Composable using Jetpack Compose Navigation

Viewed 1759

When navigating from Composable A -> Composable B, say Composable A is a Lazy List scrolled halfway down and Composable B is a Lazy List Item Details Screen. Currently, the lazy list scroll position isn't stored, and when navigating back to Composable A from B, the list starts from item index 0. We could store it in a ViewModel, and read the value back, as well as use rememberSaveable, however, I am unsure as to how to implement rememberSaveable so that it scrolls to the saved position after back navigation.

Which method would be preferred to use following good code practices?

Edit: My problem arises from the fact that the listState isn't stored when navigating back from composable B to A. So if we scroll to the bottom and select an item and look at its details, when we navigate back to the list it is scrolled to the top, instead of saving its scrollState.

My composable

        val listState = rememberLazyListState()
        val showTitle by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        onShowTitle(showTitle) // don't show title when first list element is visible


        LazyColumn(
            state = listState,
            contentPadding = PaddingValues(16.dp)
        ) {

            item {
                Text("Header 1", style = MaterialTheme.typography.h4)
            }
            item {
                Column(Modifier.fillMaxWidth()) {

                    CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                        Text(
                            "Description 1",
                            style = MaterialTheme.typography.subtitle1,
                            modifier = Modifier.padding(top = 8.dp)
                        )
                    }
                    Spacer(modifier = Modifier.height(48.dp))
                }
            }
            item {

                itemOfTheDay.value?.let { item->
                    ItemCardVertical(
                        item = item,
                        navigateToDetails = {
                            viewModel.selectItem(it)
                            navigateToDetails(it)
                        }
                    )
                    Spacer(modifier = Modifier.height(48.dp))
                }
            }
            item {
                Text("Header 2", style = MaterialTheme.typography.h5)

                Spacer(Modifier.size(16.dp))
            }
            item {


                StaggeredVerticalGrid(maxColumnWidth = 220.dp) {
                    items.value?.forEach { item->
                        ItemCard(item, navigateToDetails = {
                            viewModel.selectItem(it)
                            navigateToDetails(it)
                        })
                    }

My NavGraph

 @Composable
    fun NavGraph(startDestination: String = "Items") {
        val navController = rememberNavController()

        val mainViewModel: MainViewModel = viewModel()
        NavHost(navController = navController, startDestination = startDestination) {

            navigation(route = "Items", startDestination = MAINSCREEN) {


                composable(MAINSCREEN) {
                   
                    MainScreen(mainViewModel,
                        navigateToDetails = { smoothie ->

                            navController.navigate(ITEMSDETAILSSCREEN)
                        }
                    )
                }
                composable(
                   ITEMDETAILSSCREEN
                ) {
      
                    ItemDetails(
                        viewModel = mainViewModel, modifier = Modifier
                            .fillMaxSize()
                            .navigationBarsPadding()
                    )
                }
            }
        }
    }
2 Answers

I'm leaving this question up in case anyone else ever gets stuck in my situation, but the code works as it is meant to, I just committed a folly.

I didn't account for height changes with asynchronous image loading and as such, the list would not be at its saved position upon composable navigation, due to the list state being smaller than the screen height on returning to the composable.

However, If the images were given static containers to load into to that don't change their size, then upon back navigation, the composable would correctly display the saved list state.

I think this is a bug in compose, but I tried a very bad way to solve this problem:

  1. save the last index and scrollOffset of your clicked
  2. call scrollToItem to scroll to old position
  3. I don’t know why, it takes a certain delay to be effective. It may have something to do with the state of my code. You can try to add delay(1) according to your code.
    LaunchedEffect(state) {
        delay(1)
        state.scrollToItem(viewModel.firstVisibleIndex, viewModel.firstVisibleIndexOffset)
    }
Related