23

I am using Paging 3 lib. and i am able to check if refresh state is "Loading" or "Error" but i am not sure how to check "empty" state.I am able to add following condition but i am not sure if its proper condition

adapter.loadStateFlow.collectLatest { loadStates ->
                viewBinding.sflLoadingView.setVisibility(loadStates.refresh is LoadState.Loading)
                viewBinding.llErrorView.setVisibility(loadStates.refresh is LoadState.Error)
                viewBinding.button.setOnClickListener { pagingAdapter.refresh() }

                if(loadStates.refresh is LoadState.NotLoading && (viewBinding.recyclerView.adapter as ConcatAdapter).itemCount == 0){
                    viewBinding.llEmptyView.setVisibility(true)
                }else{
                    viewBinding.llEmptyView.setVisibility(false)
                }
            } 

Also I am running into other problem I have implemented search functionality and until more than 2 characters are entered i am using same paging source like following but the above loadstate callback is executed only once.So thats why i am not able to hide empty view if search query is cleared.I am doing so to save api call from front end.

private val originalList : LiveData<PagingData<ModelResponse>> = Transformations.switchMap(liveData){
        repository.fetchSearchResults("").cachedIn(viewModelScope)
    }

    val list : LiveData<LiveData<PagingData<ModelResponse>>> = Transformations.switchMap{ query ->
        if(query != null) {
            if (query.length >= 2)
                repository.fetchSearchResults(query)
            else
                originalList
        }else
            liveData { emptyList<ModelResponse>() }
    } 
Parth Makadia
  • 231
  • 1
  • 2
  • 4
  • What does the implementation of your `repository.fetchSearchResults(query)` look like and when are you calling submitData? How are you verifying that the callback is only called once? You should be receiving a new CombinedLoadStates anytime any of the states change (including append / prepend). To be clear, your condition checks itemCount of ConcatAdapter, but I think you want itemCount of just the PagingDataAdapter, since ConcatAdapter will include other adapters (such as LoadStateAdapter) as well. FYI: You can use `submitData(PagingData.empty())` to clear the list. – dlam Oct 15 '20 at 18:02
  • `fun fetchSearchResults(searchQuery : String): LiveData> { return Pager( config = PagingConfig(pageSize = DEFAULT_PAGE_SIZE, enablePlaceholders = false), pagingSourceFactory = { ModelPagingSource(searchQuery) } ).liveData }` – Parth Makadia Oct 16 '20 at 06:14
  • @dlam And regarding callback issue, in normal condition it is working fine. I am trying to reuse same pagingsource (originalList) if query length is less than 2 and after reusing the same obj(originalList), loadstate callback method is not executed and i think thats the expected behaviour that for one paging source, refresh loadstate will be called only till "error" or "notloading" state is achieved and will not be called again untill we make some explicit calls – Parth Makadia Oct 16 '20 at 06:24
  • And can you please explain how to check empty state in list – Parth Makadia Oct 16 '20 at 06:27
  • You're right that re-using `PagingSource` will cause issues. I would try to filter the search input instead so that you don't end up reloading when you don't want to instead of trying to "reuse" a PagingSource. For checking empty state, `PagingDataAdapter.itemCount` will work when called from loadStateFlow / listeners (load state events are guaranteed to be sent synchronously with insert events, so you can be sure the loaded page has been presented). There is also a `.snapshot()` method in case you need to view the actual items presented themselves. – dlam Oct 20 '20 at 04:31

4 Answers4

63

Here is the proper way of handling the empty view in Android Paging 3:

adapter.addLoadStateListener { loadState ->
            if (loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && adapter.itemCount < 1) {
                recycleView?.isVisible = false
                emptyView?.isVisible = true
            } else {
                recycleView?.isVisible = true
                emptyView?.isVisible = false
            }
        }
Gulzar Bhat
  • 1,255
  • 11
  • 13
  • 1
    what if there are several pages are still to be fetched? For that endOfPaginationReached will return false, that will cause emptyView to be displayed even if data is there. Correct me if i am wrong. – Daniyal Javaid Jul 01 '21 at 16:34
  • 1
    Always return endOfPaginationReached "false" – Patrick Sep 21 '21 at 14:58
  • For me `loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached` seems to be enough – PrzemekTom Nov 24 '21 at 10:17
  • @Gulzar Bhat, vai what will be the logic to show Initial loader? I mean , to show a Lottie animation at the first network call? – Aminul Haque Aome Jan 25 '22 at 08:35
  • Why do I have to check zero as `adapter.itemCount < 1`? I think it's okay to check with `adapter.itemCount == 0`. Is there any difference? – Isaac Lee Mar 07 '22 at 06:40
6
adapter.loadStateFlow.collect {
    if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached) {
        emptyState.isVisible = adapter.itemCount < 1
    }
}

The logic is,If the append has finished (it.append is LoadState.NotLoading && it.append.endOfPaginationReached == true), and our adapter items count is zero (adapter.itemCount < 1), means there is nothing to show, so we show the empty state.

PS: for initial loading you can find out more at this answer: https://stackoverflow.com/a/67661269/2472350

Amin Keshavarzian
  • 3,646
  • 1
  • 37
  • 38
3

I am using this way and it works.

EDITED: dataRefreshFlow is deprecated in Version 3.0.0-alpha10, we should use loadStateFlow now.

  viewLifecycleOwner.lifecycleScope.launch {
            transactionAdapter.loadStateFlow
                .collectLatest {
                    if(it.refresh is LoadState.NotLoading){
                        binding.textNoTransaction.isVisible = transactionAdapter.itemCount<1
                    }
                }
        }

For detailed explanation and usage of loadStateFlow, please check https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data#load-state-listener

veeyikpong
  • 829
  • 8
  • 20
2

If you are using paging 3 with Jetpack compose, I had a similar situation where I wanted to show a "no results" screen with the Pager 3 library. I wasn't sure what the best approach was in Compose but I use now this extension function on LazyPagingItems. It checks if there are no items and makes sure there are no items coming by checking if endOfPaginationReached is true.

private val <T : Any> LazyPagingItems<T>.noItems
    get() = loadState.append.endOfPaginationReached && itemCount == 0

Example usage is as follows:

@Composable
private fun ArticlesOverviewScreen(screenContent: Content) {
    val lazyBoughtArticles = screenContent.articles.collectAsLazyPagingItems()
    
    when {
        lazyBoughtArticles.noItems -> NoArticlesScreen()
        else -> ArticlesScreen(lazyBoughtArticles)
    }
}

Clean and simple :).

moallemi
  • 2,448
  • 2
  • 25
  • 28
Ronald
  • 584
  • 4
  • 7
  • Can you describe more? – Ashton Jan 04 '23 at 04:08
  • I've described a bit more. – Ronald Jan 04 '23 at 12:31
  • Do you put the `private val` code in the `ViewModel`, and after collecting the lazypagingitems in the composable, it says `.noItems` is an unresolved reference. What can I do to fix this? Thanks. – Raj Narayanan Jan 11 '23 at 10:48
  • @RajNarayanan, I use the extension function in my Composable so the code is not in my ViewModel but in the file containing the relevant Composables. – Ronald Jan 13 '23 at 13:05
  • @Ronald The extension function is unusable in a composable. It throws multiple `unresolved reference` errors which I cannot resolve. – Raj Narayanan Jan 14 '23 at 11:05
  • I think your problem might be somewhere else in your code. I can use it without any problems. Are you sure that you have this piece code in the same module and file where your composable is? And did you check your dependencies? Using this requires a dependency for compose: "androidx.paging:paging-compose"? – Ronald Jan 16 '23 at 12:56
  • @Ronald I placed the extension outside of the composable but inside the same file as the composable, right above the `@Composable` declaration. It's working now. And I tried to edit your answer to show where exactly the extension is placed, but it said the edit que was full, and I wasn't able to submit it. – Raj Narayanan Jan 23 '23 at 12:39