androidandroid-recyclerviewandroid-pagingshimmer

How to distinguish between initial load and content change in Android Paging 3?


I have a RecyclerView that uses a PagingDataAdapter to show its items. On initial load of the entire page, I want to show a Shimmer loading placeholder. However, when I try to do this, the loading placeholder also shows up for a small content change of a single item. This makes the entire screen flicker because it hides the RecyclerView during loading and reshows it once the content change has been loaded. I don't want the loading placeholder to show for a single content change.

I am checking the loading state in the load state listener for the adapter:

addLoadStateListener {
    val taskListState = when (it.refresh) { // triggered for both initial load and content change
        is LoadState.Loading -> ScheduleViewModel.TaskListState.LOADING
        // I want something like if (loadStateIsForInitialLoad()) ScheduleViewModel.TaskListState.LOADING else ScheduleViewModel.TaskListState.PRESENT
        is LoadState.Error -> ScheduleViewModel.TaskListState.ERROR
        is LoadState.NotLoading ->
            if (it.append.endOfPaginationReached && itemCount < 1) {
                ScheduleViewModel.TaskListState.EMPTY
            } else {
                ScheduleViewModel.TaskListState.PRESENT
            }
    }
    scheduleViewModel.setTaskListState(taskListState)
}

The content change goes through the database:

class ScheduleViewModel @Inject constructor(private val taskRepository: TaskRepository) :
        ViewModel() {

  fun updateDoneState(task: TaskItem) {
    viewModelScope.launch(Dispatchers.IO) {
        if (task.isDone) {
            taskRepository.markUndone(task.id)
        } else {
            taskRepository.markDone(task.id)
        }
    }
  }
}

Layout:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="viewModel" type="ogbe.eva.prompt.ui.schedule.ScheduleViewModel"/>
    </data>
    <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <com.facebook.shimmer.ShimmerFrameLayout
                android:id="@+id/shimmerFrameLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:isVisible="@{viewModel.isLoading}">

            <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">

                <include layout="@layout/item_task_placeholder"/>

                <include layout="@layout/item_task_placeholder"/>

                <include layout="@layout/item_task_placeholder"/>

                <include layout="@layout/item_task_placeholder"/>

                <include layout="@layout/item_task_placeholder"/>

            </LinearLayout>

        </com.facebook.shimmer.ShimmerFrameLayout>

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/task_list"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="@dimen/spacing_sm"
                app:isVisible="@{viewModel.isPresent}"/>

    </FrameLayout>
</layout>

It looks like CombinedLoadStates only has one refresh load state for both initial load and content changes. Is there some other way to distinguish them?


Solution

  • Are you using RemoteMediator? If so, you can just observe changes to CombinedLoadStates.mediator.refresh. CombinedLoadStates.refresh is just a helper which combiens both mediator + source states for "common" use-case.

    If you're only using PagingSource and updating DB / invalidating separately from RemoteMediator, you can also check against adapter.itemCount to determine "initial load".

    adapter.addLoadStateListener { loadStates ->
      when (loadStates.source.refresh) {
        is NotLoading -> {
          // Always redundantly disable loading spinner here, which assumes doing so
          // is stateless.
          scheduleViewModel.setTaskListState(
            if (it.append.endOfPaginationReached && itemCount < 1) {
              ScheduleViewModel.TaskListState.EMPTY
            } else {
              ScheduleViewModel.TaskListState.PRESENT
            }
          )
        }
        is LoadState.Loading -> {
          if (adapter.itemCount == 0) {
            ScheduleViewModel.TaskListState.LOADING
          } else {
            ScheduleViewModel.TaskListState.PRESENT
          }
        }
        is LoadState.Error -> ScheduleViewModel.TaskListState.ERROR
      }
    }
    

    Otherwise, you can also just track this yourself and mix-in with loadStateFlow, but kind of need to define a bit more precisely what you mean by initial load. For example does configuration change or process death count as initial load or do you just care about non-cached (in memory and db) case?