android-jetpack-composekotlin-multiplatformcompose-multiplatform

LazyVerticalGrid with BottomBar don't save scroll state


It's a Compose Multiplatform project. I have a screen with BottomBar component.

fun HomeScreen(mainNavController: NavHostController) {
    val homeViewModel = koinViewModel<HomeViewModel>()

    val navController = rememberNavController()
    val items = listOf(BottomBarItem.Episodes(), BottomBarItem.Characters())

    Scaffold(bottomBar = {
        BottomNavigation {

            val navBackStackEntry by navController.currentBackStackEntryAsState()
            val currentDestination = navBackStackEntry?.destination

            items.forEach { item ->
                BottomNavigationItem(icon = { item.icon },
                    label = { Text(item.title) },
                    selected = currentDestination?.hierarchy?.any { it.route == item.route } == true,
                    onClick = {
                        navController.navigate(item.route) {
                            navController.graph.startDestinationRoute?.let { route ->
                                popUpTo(route) {
                                    saveState = true
                                }
                            }
                            // Avoid multiple copies of the same destination when
                            // reselecting the same item
                            launchSingleTop = true
                            // Restore state when reselecting a previously selected item
                            restoreState = true


                        }

                    })
            }
        }
    }) {
        Box(
            modifier = Modifier.padding(it)
        ) {
            Image(
                painter = painterResource(Res.drawable.space),
                contentDescription = "background",
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Crop
            )
            NavigationBottom(navController, mainNavController)
        }
    }

The navigation works fine, and I now every navigation to CharacterScreen is the same instance because I have a log on my ViewModel to verify I not create a new instance of this.

class CharactersViewModel(val getRandomCharacter: GetRandomCharacter, repository: Repository) : ViewModel() {

    private val _state = MutableStateFlow(CharactersState())
    val state: StateFlow<CharactersState> = _state

    val characters: Flow<PagingData<CharacterModel>> = repository.getAllCharacters()

    init {
        Napier.i { "Created" }
        viewModelScope.launch {
            val character = withContext(Dispatchers.IO) {
                getRandomCharacter()
            }
            _state.update { it.copy(randomCharacter = character) }
        }
    }

}

data class CharactersState(
    val randomCharacter: CharacterModel? = null
)

The problem is the screen, Its keep the content but ignore the scroll remember so every time I return to Screen the list is on position 0.

fun CharactersScreen(navigateToCharacterDetail: (Int) -> Unit) {

    val charactersViewModel = koinViewModel<CharactersViewModel>()

    val state = charactersViewModel.state.collectAsState()
    val characters = charactersViewModel.characters.collectAsLazyPagingItems()

    val lazyGridState = rememberLazyGridState()

    LazyVerticalGrid(
        modifier = Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp),
        columns = GridCells.Fixed(2),
        state = lazyGridState,
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        item(span = { GridItemSpan(2) }) {
            Column {
                Text(
                    "Characters",
                    color = Color.White,
                    fontSize = 24.sp,
                    modifier = Modifier.padding(vertical = 8.dp)
                )
                RandomCharacter(character = state.value.randomCharacter)
            }
        }

        when {
            //Carga inicial
            characters.loadState.refresh is LoadState.Loading && characters.itemCount == 0 -> {
                item (span = { GridItemSpan(2) }){
                    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(64.dp), color = Color.Red
                        )
                    }
                }
            }

            //Estado vacio

            characters.loadState.refresh is LoadState.NotLoading && characters.itemCount == 0 -> {
                item {
                    Text(text = "Todavía no hay personajes")
                }
            }

            else -> {
                items(characters.itemCount, span = { GridItemSpan(1) }) {
                    characters[it]?.let { characterModel ->
                        ItemList(characterModel) { navigateToCharacterDetail(it) }
                    }
                }

                if (characters.loadState.append is LoadState.Loading) {
                    item {
                        Box(
                            modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center
                        ) {
                            CircularProgressIndicator(
                                modifier = Modifier.size(64.dp), color = Color.White
                            )
                        }
                    }
                }
            }
        }
    }
}


I thought it was because of the paging but I tried with a simple list and still have the same error.

I tried to create a custom saver, use different remembers and still have the same problem.


Solution

  • The problem you are describing seems very similar to Issue #177245496 on the Google Issue Tracker.

    What might be happening is that the collectAsLazyPagingItems returns an empty List for a very short moment when returning to that Composable. That short moment where the List is empty is enough to reset the scroll position.

    There are several solutions and workarounds discussed in the linked issue. One official solution provided by the Jetpack Compose Team is the cachedIn function. It caches any previously loaded list items until loading the up-to-date ones is finished.

    Please use the following code:

    val characters: Flow<PagingData<CharacterModel>> = 
        repository.getAllCharacters().cachedIn(viewModelScope)