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?
Are you using
RemoteMediator? If so, you can just observe changes toCombinedLoadStates.mediator.refresh.CombinedLoadStates.refreshis just a helper which combiens both mediator + source states for "common" use-case.If you're only using
PagingSourceand updating DB / invalidating separately fromRemoteMediator, you can also check againstadapter.itemCountto determine "initial load".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?