How to prevent entire composable from recomposing when popping backstack in navigation composable

833 Views Asked by At

What currently is happening:

  • I have a grid screen and on clicking on the list I navigate to the detail screen
  • Now on clicking the back button I navigate back to the list screen

What is the issue:

  • If I have scrolled to the 20th item and navigated to detail and pressed the back button
  • Once I navigate back entire screen is recomposed in a grid screen and as a result, I scroll to the top as if entire screen is reloaded

How to prevent this so I remain in the 20th item


MainActivity.kt

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PokedexTheme {
                // Always create one nav controller and pass into the nav-host
                val navController = rememberNavController()
                NavHost(
                    navController = navController,
                    startDestination = "pokemon_list_screen"
                ) {
                    composable("pokemon_list_screen") {
                        PokemonListScreen(navController = navController)
                    }
                    composable(
                        route = "pokemon_detail_screen/{dominantColor}/{pokemonName}",
                        arguments = listOf(
                            navArgument("dominantColor") {
                                type = NavType.IntType
                            },
                            navArgument("pokemonName") {
                                type = NavType.StringType
                            }
                        )
                    ) {
                        val dominantColor = remember {
                            val color = it.arguments?.getInt("dominantColor")
                            color?.let { Color(it) } ?: Color.White
                        }
                        val pokemonName = remember {
                            it.arguments?.getString("pokemonName")
                        }
                        PokemonDetailScreen(
                            dominantColor = dominantColor,
                            pokemonName = pokemonName?.lowercase(Locale.ROOT) ?: "",
                            navController = navController
                        )
                    }
                }
            }
        }
    }
}

PokemonListScreen.kt

@Composable
fun PokemonListScreen(
    navController: NavController
) {
    Column(
        modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)
    ) {
        Spacer(modifier = Modifier.height(20.dp))
        PokemonBanner()
        PokemonLazyList(
            onItemClick = { entry ->
                navController.navigate(
                    "pokemon_detail_screen/${entry.dominentColor.toArgb()}/${entry.pokemonName}"
                )
            }
        )
    }
}

PokemonDetailScreen.kt

@Composable
fun PokemonDetailScreen(
    dominantColor: Color,
    pokemonName: String,
    navController: NavController,
    topPadding: Dp = 20.dp,
    pokemonImageSize: Dp = 200.dp,
    viewModel: PokemonDetailVm = hiltViewModel()
) {

    var pokemonDetailData by remember { mutableStateOf<PokemonDetailView>(PokemonDetailView.DisplayLoadingView) }
    val pokemonDetailScope = rememberCoroutineScope()


    LaunchedEffect(key1 = true){
        viewModel.getPokemonDetails(pokemonName)
        viewModel.state.collect{ pokemonDetailData = it }
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(dominantColor)
            .padding(bottom = 16.dp)
    ) {

        PokemonHeader(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight(0.2f)
                .align(Alignment.TopCenter)
        ) {
            navController.popBackStack()
        }

        PokemonBody(
            pokemonInfo = pokemonDetailData,
            topPadding = topPadding,
            pokemonImageSize = pokemonImageSize
        ) {
            pokemonDetailScope.launch {
                viewModel.getPokemonDetails(pokemonName)
            }
        }

        Box(
            contentAlignment = Alignment.TopCenter,
            modifier = Modifier
                .fillMaxSize()
        ) {


            if(pokemonDetailData is PokemonDetailView.DisplayPokemonView){

                val data = (pokemonDetailData as PokemonDetailView.DisplayPokemonView).data

                data.sprites.let {
                    // Image is available
                    val url = PokemonUtils.formatPokemonDetailUrl(it.frontDefault)
                    AsyncImage(
                        model = ImageRequest.Builder(LocalContext.current)
                            .data(url)
                            .crossfade(true)
                            .build(),
                        contentDescription = data.name,
                        contentScale = ContentScale.Fit,
                        modifier = Modifier
                            // Set the default size passed to the composable
                            .size(pokemonImageSize)
                            // Shift the image down from the top
                            .offset(y = topPadding)
                    )
                }
            }
        }
    }

}

New Code Added

PokemonListVm.kt

@HiltViewModel
class PokemonListVm @Inject constructor(
    private val repository: PagingRepository
): ViewModel() {

    fun getPokemonList(): Flow<PagingData<PokedexListEntry>> = repository.getPokemon().cachedIn(viewModelScope)


    /**
     * What it does: It calculates the dominant color based on a drawable
     * What it returns: Color as a function callback
     * @param drawable
     * @param onFinish
     */
    fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit) {
        val bmp = (drawable as BitmapDrawable).bitmap.copy(Bitmap.Config.ARGB_8888, true)

        Palette.from(bmp).generate { palette ->
            palette?.dominantSwatch?.rgb?.let { colorValue ->
                onFinish(Color(colorValue))
            }
        }
    }

}

PokemonLazyList.kt

@Composable
fun PokemonLazyList(
    onItemClick:(PokedexListEntry)-> Unit,
    viewModel: PokemonListVm = hiltViewModel(),
    loadingScreenState:() -> Unit = {},
    errorScreenState:() -> Unit = {},
){

    val pokemonList =  viewModel.getPokemonList().collectAsLazyPagingItems()
    val state = rememberLazyGridState()
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        state = state
    ) {

        itemsCustom(pokemonList){
            if (it != null) {
                PokemonListItem(
                    item=it,
                    onItemClick=onItemClick
                )
            }
        }

        pokemonList.apply {
            when {
                loadState.refresh is LoadState.Loading -> {
                    item { LoadingView(modifier = Modifier.fillMaxSize()) }
                }
                loadState.append is LoadState.Loading -> {
                    item { LoadingView(modifier = Modifier.fillMaxSize()) }
                }
                loadState.refresh is LoadState.Error -> {
                    val e = pokemonList.loadState.refresh as LoadState.Error
                    item {
                        ErrorItem(
                            message = e.error.localizedMessage!!,
                            modifier = Modifier.fillMaxSize(),
                            onClickRetry = { retry() }
                        )
                    }
                }
                loadState.append is LoadState.Error -> {
                    val e = pokemonList.loadState.append as LoadState.Error
                    item {
                        ErrorItem(
                            message = e.error.localizedMessage!!,
                            onClickRetry = { retry() }
                        )
                    }
                }
            }
        }
    }
}

Full-SourceCode: Code

1

There are 1 best solutions below

3
On

One option is to use LazyGridState.

In PokemonListScreen:

val viewModel: PokemonListVm = hiltViewModel()
val gridState = rememberLazyGridState(firstVisibleItemIndex = viewModel.savedIndex)
...
PokemonLazyList(
    state = gridState
    onItemClick = { entry ->
        viewModel.savedIndex = gridState.firstVisibleItemIndex
...

In PokemonLazyList pass that state to the underlying Grid:

...
LazyVerticalGrid(state = gridSate ...
...

The state must be saved somewhere, like PokemonListVm:

...
var savedIndex = 0
...