androidkotlinpaginationandroid-jetpack-composekotlin-stateflow

How to stop the screen flicker/refresh for LazyVerticalgrid with pagination?


When the first page results (20) are scrolled down and when there are new results from the next page the screen flickers/refreshes. Tried many things but no idea how to stop this screen flickering.

Screen:

@Composable
fun FilmsListScreen(
    viewModel: FilmsListViewModel = hiltViewModel(),
    selectedItem: (Long) -> Unit
) {
    val filmState = viewModel.filmStateFlow.collectAsState().value
    val isLoadingMore = viewModel.isLoadingMore
    val gridState = rememberLazyGridState()

    when (filmState) {
        is ViewState.Success -> {
            val list = filmState.data
            LazyVerticalGrid(
                columns = GridCells.Fixed(2),
                modifier = Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.background)
                    .padding(8.dp),
                state = gridState
            ) {
                items(list, key = { it.id }) { film ->
                    FilmGridCardUI(film, selectedItem)
                }

                if (isLoadingMore) {
                    item {
                        LoadingView()
                    }
                } else {
                    item {
                        viewModel.loadNextPage()
                    }
                }
            }
        }

        is ViewState.Error -> {
            ErrorView(reason = filmState.message)
        }

        else -> {}
    }
}

ViewModel:

@HiltViewModel
class FilmsListViewModel @Inject constructor(
    private val filmsListUseCase: FilmsListUseCase
) : ViewModel() {

    private val _filmStateFlow: MutableStateFlow<ViewState<List<Film>>> =
        MutableStateFlow(ViewState.Idle)
    val filmStateFlow = _filmStateFlow.asStateFlow()

    // Pagination state
    private var currentPage = 1
    var isLoadingMore = false
    private var allFilms: MutableList<Film> = mutableListOf()

    init {
        getFilmsList(currentPage)
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun getFilmsList(page: Int = 1) {
         viewModelScope.launch {
             _filmStateFlow.value = ViewState.Loading
            isLoadingMore = true

            filmsListUseCase(page).collect {
                when (it) {
                    is Result.Error -> {
                        _filmStateFlow.value = ViewState.Error(it.throwable.message ?: "error loading")
                        isLoadingMore = false
                    }

                    is Result.Success -> {
                        val filmsList = it.data
                        allFilms.addAll(filmsList)
                        _filmStateFlow.value = ViewState.Success(allFilms)
                        currentPage++
                        isLoadingMore = false
                    }
                }
            }
        }
    }

    // Function to load the next page of films
    fun loadNextPage() {
        getFilmsList(currentPage)
    }
}

Solution

  • FilmsListScreen displays blank screen when filmState == ViewState.Loading:

        when (filmState) {
            is ViewState.Success -> //...
            is ViewState.Error -> //...
            else -> {} // ViewState.Loading goes here
        }
    

    Judging by presence of LoadingView() in FilmsListScreen you want to display a loading indicator below the film items instead. To do that first you need to remove _filmStateFlow.value = ViewState.Loading.

    Then there is another issue - incorrect _filmStateFlow updating. If ViewState.Success is a data class with a single property, doing this: _filmStateFlow.value = ViewState.Success(allFilms) won't trigger an update, because those two instances of ViewState.Success contain the same list instance - allFilms and therefore are considered equal. So you need a new List instance, something like:

    // no need to modify the list, we will replace it
    private var allFilms: List<Film> = emptyList()
    
    //....
    
    val newList = allFilms + filmsList
    _filmStateFlow.value = ViewState.Success(newList)
    allFilms = newList