5

My current android application uses Room Paging3 library

implementation 'androidx.paging:paging-runtime-ktx:3.0.0-beta01'

My requirements are to display either a "Data Loading Spinner", an Empty message when no data is available, or the loaded data.

I can successfully show a loading spinner or the data when loaded, however when there's no data to display it does not work.

I've tried the solutions offered on SO, however none of them resolve my issue

The version of my code that successfully handles Loading and Loaded data resembles this:-

viewLifecycleOwner.lifecycleScope.launch {
            adapter.loadStateFlow
                .distinctUntilChangedBy { it.refresh }
                .filter { it.refresh is LoadState.NotLoading }
                .flowOn(Dispatchers.Default)
                .collectLatest {
                    withContext(Dispatchers.Main) {
                        if (adapter.itemCount == 0) (viewModel as HomeViewModel).loading.value = View.VISIBLE
                        else (viewModel as HomeViewModel).loading.value = View.GONE
                    }
                }
        }

I've tried to also show the empty state using this voted for SO answer, however it does not work for me

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
            }
        }

The issue is caused by the loadStateFlow emitting this condition

(loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && adapter.itemCount < 1)

while data is being loaded.

My list screen has pull to refresh, during the data refresh process I first delete all existing rows, then fetch all data remotely.

Is there anyway I can use PagingDataAdapter.loadStateFlow to show my three states of Loading, Loaded or Empty?

My ViewModel code resembles this:-

repository.myData()
          .map { pagingData ->
                 pagingData.map { myDO -> myDO.mapUI() }
               }
           .cachedIn(viewModelScope)
           .collect {
                emit(it)
                    }

My Repository code resembles this:-

fun myData(): Flow<PagingData<MyDO>> {
        return Pager(
            PagingConfig(pageSize = 60, prefetchDistance = 30, enablePlaceholders = false, maxSize = 200)
        ) {
            database.myDAO().fetchMyData()
        }.flow.flowOn(Dispatchers.IO)
    }

My Room query resembles this

@Query("SELECT * from my_table ORDER BY position_date DESC")
fun fetchMyData(): PagingSource<Int, MyDO>

Where have I gone wrong?

Is it possible to achieve my desired functionality?

UPDATE

I believe that I have two separate issues.

The first is caused by my initial data sync, where I employ Android Workers to download data from a remote source and insert into my local Room database. Due to how the remote data is organised and the available Restful API's, the data displayed in a list can be inserted by multiple workers. This "partitioning" of the data inserted locally, results in the data submitted to my Paging3 adapter to be sporadic and means the Load State cycles through Loading -> Not Loading -> Loading -> Not Loading.

I display the list Fragment while the sync is on going, which results in the screen contents "Flashing" between the loading Spinner and the displayed data list

Hector
  • 4,016
  • 21
  • 112
  • 211

2 Answers2

8

I have developed a method to retrieve loading, empty, and error states from an instance of CombinedLoadStates. You can use it like the following:

adapter.addLoadStateListener { loadState ->
    loadState.decideOnState(
        showLoading = { visible ->
            (viewModel as HomeViewModel).loading.value =
                if (visible) View.VISIBLE else View.GONE
        },
        showEmptyState = { visible ->
            emptyView?.isVisible = visible
            recycleView?.isVisible = !visible
        },
        showError = { message ->
            Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
        }
    )
}
private inline fun CombinedLoadStates.decideOnState(
    showLoading: (Boolean) -> Unit,
    showEmptyState: (Boolean) -> Unit,
    showError: (String) -> Unit
) {
    showLoading(refresh is LoadState.Loading)

    showEmptyState(
        source.append is LoadState.NotLoading
                && source.append.endOfPaginationReached
                && adapter.itemCount == 0
    )

    val errorState = source.append as? LoadState.Error
        ?: source.prepend as? LoadState.Error
        ?: source.refresh as? LoadState.Error
        ?: append as? LoadState.Error
        ?: prepend as? LoadState.Error
        ?: refresh as? LoadState.Error

    errorState?.let { showError(it.error.toString()) }
}
aminography
  • 21,986
  • 13
  • 70
  • 74
1

I could not do it without actually checking the local db data, so I decided to actually check the local db if the data is empty when the refreshing action is made, like this:

val isRefreshLoading = states.refresh is LoadState.Loading
val isEmptyViewVisible = !isRefreshLoading && viewModel.isUnreadEmpty()

binding.refresher.isRefreshing = isRefreshLoading
binding.emptyView.isVisible = isEmptyViewVisible

ViewModel.isUnreadEmpty() checks if my data in the db is empty by calling room dao function.

shoheikawano
  • 1,092
  • 3
  • 14
  • 31