PagingDataAdapter

public abstract class PagingDataAdapter<T extends Object, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter


RecyclerView.Adapter base class for presenting paged data from PagingDatas in a RecyclerView.

This class is a convenience wrapper around AsyncPagingDataDiffer that implements common default behavior for item counting, and listening to update events.

To present a Pager, use collectLatest to observe Pager.flow and call submitData whenever a new PagingData is emitted.

If using RxJava and LiveData extensions on Pager, use the non-suspending overload of submitData, which accepts a Lifecycle.

PagingDataAdapter listens to internal PagingData loading events as pages are loaded, and uses DiffUtil on a background thread to compute fine grained updates as updated content in the form of new PagingData objects are received.

State Restoration: To be able to restore RecyclerView state (e.g. scroll position) after a configuration change / application recreate, PagingDataAdapter calls RecyclerView.Adapter.setStateRestorationPolicy with RecyclerView.Adapter.StateRestorationPolicy.PREVENT upon initialization and waits for the first page to load before allowing state restoration. Any other call to RecyclerView.Adapter.setStateRestorationPolicy by the application will disable this logic and will rely on the user set value.

val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
        // User ID serves as unique ID
        oldItem.userId == newItem.userId

    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
        // Compare full contents (note: Java users should call .equals())
        oldItem == newItem
}

class UserAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        return UserViewHolder.create(parent)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val repoItem = getItem(position)
        // Note that item may be null, ViewHolder must support binding null item as placeholder
        holder.bind(repoItem)
    }
}

Summary

Public constructors

<T extends Object, VH extends RecyclerView.ViewHolder> PagingDataAdapter(
    @NonNull DiffUtil.ItemCallback<@NonNull T> diffCallback,
    @NonNull CoroutineContext mainDispatcher,
    @NonNull CoroutineContext workerDispatcher
)

Public methods

final void
addLoadStateListener(
    @NonNull Function1<@NonNull CombinedLoadStatesUnit> listener
)

Add a CombinedLoadStates listener to observe the loading state of the current PagingData.

final void
addOnPagesUpdatedListener(@NonNull Function0<Unit> listener)

Add a listener which triggers after the pages presented to the UI are updated, even if the actual items presented don't change.

int

Returns the total number of items in the data set held by the adapter.

final long
getItemId(int position)

Note: getItemId is final, because stable IDs are unnecessary and therefore unsupported.

final @NonNull Flow<@NonNull CombinedLoadStates>

A hot Flow of CombinedLoadStates that emits a snapshot whenever the loading state of the current PagingData changes.

final @NonNull Flow<Unit>

A hot Flow that emits after the pages presented to the UI are updated, even if the actual items presented don't change.

final T
@MainThread
peek(@IntRange(from = 0) int index)

Returns the presented item at the specified position, without notifying Paging of the item access that would normally trigger page loads.

final void

Refresh the data presented by this PagingDataAdapter.

final void

Remove a previously registered CombinedLoadStates listener.

final void

Remove a previously registered listener for new PagingData generations completing initial load and presenting to the UI.

final void

Retry any failed load requests that would result in a LoadState.Error update to this PagingDataAdapter.

final void
setHasStableIds(boolean hasStableIds)

Stable ids are unsupported by PagingDataAdapter.

void

Sets the state restoration strategy for the Adapter.

final @NonNull ItemSnapshotList<@NonNull T>

Returns a new ItemSnapshotList representing the currently presented items, including any placeholders if they are enabled.

final void

Present a PagingData until it is invalidated by a call to refresh or PagingSource.invalidate.

final void
submitData(
    @NonNull Lifecycle lifecycle,
    @NonNull PagingData<@NonNull T> pagingData
)

Present a PagingData until it is either invalidated or another call to submitData is made.

final @NonNull ConcatAdapter

Create a ConcatAdapter with the provided LoadStateAdapters displaying the LoadType.APPEND as a list item at the start of the presented list.

final @NonNull ConcatAdapter

Create a ConcatAdapter with the provided LoadStateAdapters displaying the LoadType.PREPEND as a list item at the end of the presented list.

final @NonNull ConcatAdapter

Create a ConcatAdapter with the provided LoadStateAdapters displaying the LoadType.PREPEND and LoadType.APPENDs as list items at the start and end respectively.

Protected methods

final T
@MainThread
getItem(@IntRange(from = 0) int position)

Returns the presented item at the specified position, notifying Paging of the item access to trigger any loads necessary to fulfill prefetchDistance.

Inherited methods

From androidx.recyclerview.widget.RecyclerView.Adapter
final void
bindViewHolder(@NonNull VH holder, int position)

This method internally calls onBindViewHolder to update the ViewHolder contents with the item at the given position and also sets up some private fields to be used by RecyclerView.

final @NonNull VH
createViewHolder(@NonNull ViewGroup parent, int viewType)

This method calls onCreateViewHolder to create a new ViewHolder and initializes some private fields to be used by RecyclerView.

int

Returns the position of the given ViewHolder in the given Adapter.

int
getItemViewType(int position)

Return the view type of the item at position for the purposes of view recycling.

final @NonNull RecyclerView.Adapter.StateRestorationPolicy

Returns when this Adapter wants to restore the state.

final boolean

Returns true if one or more observers are attached to this adapter.

final boolean

Returns true if this adapter publishes a unique long value that can act as a key for the item at a given position in the data set.

final void

Notify any registered observers that the data set has changed.

final void
notifyItemChanged(int position)

Notify any registered observers that the item at position has changed.

final void
notifyItemChanged(int position, @Nullable Object payload)

Notify any registered observers that the item at position has changed with an optional payload object.

final void
notifyItemInserted(int position)

Notify any registered observers that the item reflected at position has been newly inserted.

final void
notifyItemMoved(int fromPosition, int toPosition)

Notify any registered observers that the item reflected at fromPosition has been moved to toPosition.

final void
notifyItemRangeChanged(int positionStart, int itemCount)

Notify any registered observers that the itemCount items starting at position positionStart have changed.

final void
notifyItemRangeChanged(
    int positionStart,
    int itemCount,
    @Nullable Object payload
)

Notify any registered observers that the itemCount items starting at position positionStart have changed.

final void
notifyItemRangeInserted(int positionStart, int itemCount)

Notify any registered observers that the currently reflected itemCount items starting at positionStart have been newly inserted.

final void
notifyItemRangeRemoved(int positionStart, int itemCount)

Notify any registered observers that the itemCount items previously located at positionStart have been removed from the data set.

final void
notifyItemRemoved(int position)

Notify any registered observers that the item previously located at position has been removed from the data set.

void

Called by RecyclerView when it starts observing this Adapter.

abstract void
onBindViewHolder(@NonNull VH holder, int position)

Called by RecyclerView to display the data at the specified position.

void
onBindViewHolder(
    @NonNull VH holder,
    int position,
    @NonNull List<@NonNull Object> payloads
)

Called by RecyclerView to display the data at the specified position.

abstract @NonNull VH
onCreateViewHolder(@NonNull ViewGroup parent, int viewType)

Called when RecyclerView needs a new ViewHolder of the given type to represent an item.

void

Called by RecyclerView when it stops observing this Adapter.

boolean

Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled due to its transient state.

void

Called when a view created by this adapter has been attached to a window.

void

Called when a view created by this adapter has been detached from its window.

void
onViewRecycled(@NonNull VH holder)

Called when a view created by this adapter has been recycled.

void

Register a new observer to listen for data changes.

void

Unregister an observer currently listening for data changes.

Public constructors

PagingDataAdapter

public <T extends Object, VH extends RecyclerView.ViewHolder> PagingDataAdapter(
    @NonNull DiffUtil.ItemCallback<@NonNull T> diffCallback,
    @NonNull CoroutineContext mainDispatcher,
    @NonNull CoroutineContext workerDispatcher
)

Public methods

addLoadStateListener

Added in 3.0.0
public final void addLoadStateListener(
    @NonNull Function1<@NonNull CombinedLoadStatesUnit> listener
)

Add a CombinedLoadStates listener to observe the loading state of the current PagingData.

As new PagingData generations are submitted and displayed, the listener will be notified to reflect the current CombinedLoadStates.

val adapter = UserPagingAdapter()
adapter.addLoadStateListener {
    // show a retry button outside the list when refresh hits an error
    retryButton.isVisible = it.refresh is LoadState.Error

    // swipeRefreshLayout displays whether refresh is occurring
    swipeRefreshLayout.isRefreshing = it.refresh is LoadState.Loading

    // show an empty state over the list when loading initially, before items are loaded
    emptyState.isVisible = it.refresh is LoadState.Loading && adapter.itemCount == 0
}
Parameters
@NonNull Function1<@NonNull CombinedLoadStatesUnit> listener

LoadStates listener to receive updates.

addOnPagesUpdatedListener

Added in 3.1.0
public final void addOnPagesUpdatedListener(@NonNull Function0<Unit> listener)

Add a listener which triggers after the pages presented to the UI are updated, even if the actual items presented don't change.

An update is triggered from one of the following:

  • submitData is called and initial load completes, regardless of any differences in the loaded data

  • A Page is inserted

  • A Page is dropped

Parameters
@NonNull Function0<Unit> listener

called after pages presented are updated.

getItemCount

Added in 3.0.0
public int getItemCount()

Returns the total number of items in the data set held by the adapter.

Returns
int

The total number of items in this adapter.

getItemId

Added in 3.0.0
public final long getItemId(int position)

Note: getItemId is final, because stable IDs are unnecessary and therefore unsupported.

PagingDataAdapter's async diffing means that efficient change animations are handled for you, without the performance drawbacks of RecyclerView.Adapter.notifyDataSetChanged. Instead, the diffCallback parameter of the PagingDataAdapter serves the same functionality - informing the adapter and RecyclerView how items are changed and moved.

getLoadStateFlow

Added in 3.0.0
public final @NonNull Flow<@NonNull CombinedLoadStatesgetLoadStateFlow()

A hot Flow of CombinedLoadStates that emits a snapshot whenever the loading state of the current PagingData changes.

This flow is conflated, so it buffers the last update to CombinedLoadStates and immediately delivers the current load states on collection.

getOnPagesUpdatedFlow

Added in 3.1.0
public final @NonNull Flow<UnitgetOnPagesUpdatedFlow()

A hot Flow that emits after the pages presented to the UI are updated, even if the actual items presented don't change.

An update is triggered from one of the following:

  • submitData is called and initial load completes, regardless of any differences in the loaded data

  • A Page is inserted

  • A Page is dropped

Note: This is a SharedFlow configured to replay 0 items with a buffer of size 64. If a collector lags behind page updates, it may trigger multiple times for each intermediate update that was presented while your collector was still working. To avoid this behavior, you can conflate this Flow so that you only receive the latest update, which is useful in cases where you are simply updating UI and don't care about tracking the exact number of page updates.

peek

Added in 3.0.0
@MainThread
public final T peek(@IntRange(from = 0) int index)

Returns the presented item at the specified position, without notifying Paging of the item access that would normally trigger page loads.

Parameters
@IntRange(from = 0) int index

Index of the presented item to return, including placeholders.

Returns
T

The presented item at position index, null if it is a placeholder.

refresh

Added in 3.0.0
public final void refresh()

Refresh the data presented by this PagingDataAdapter.

refresh triggers the creation of a new PagingData with a new instance of PagingSource to represent an updated snapshot of the backing dataset. If a RemoteMediator is set, calling refresh will also trigger a call to RemoteMediator.load with LoadType to allow RemoteMediator to check for updates to the dataset backing PagingSource.

Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh. Invalidation due repository-layer signals, such as DB-updates, should instead use PagingSource.invalidate.

class MyActivity : AppCompatActivity() {
    private lateinit var binding: MyActivityBinding
    private val pagingAdapter = UserPagingAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = MyActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.recyclerView.adapter = pagingAdapter
        pagingAdapter.addLoadStateListener { loadStates ->
            binding.swipeRefreshLayout.isRefreshing = loadStates.refresh is LoadState.Loading
        }

        binding.swipeRefreshLayout.setOnRefreshListener {
            pagingAdapter.refresh()
        }
    }
}
See also
invalidate

removeLoadStateListener

Added in 3.0.0
public final void removeLoadStateListener(
    @NonNull Function1<@NonNull CombinedLoadStatesUnit> listener
)

Remove a previously registered CombinedLoadStates listener.

Parameters
@NonNull Function1<@NonNull CombinedLoadStatesUnit> listener

Previously registered listener.

removeOnPagesUpdatedListener

Added in 3.1.0
public final void removeOnPagesUpdatedListener(@NonNull Function0<Unit> listener)

Remove a previously registered listener for new PagingData generations completing initial load and presenting to the UI.

Parameters
@NonNull Function0<Unit> listener

Previously registered listener.

retry

Added in 3.0.0
public final void retry()

Retry any failed load requests that would result in a LoadState.Error update to this PagingDataAdapter.

Unlike refresh, this does not invalidate PagingSource, it only retries failed loads within the same generation of PagingData.

LoadState.Error can be generated from two types of load requests:

setHasStableIds

Added in 3.0.0
public final void setHasStableIds(boolean hasStableIds)

Stable ids are unsupported by PagingDataAdapter. Calling this method is an error and will result in an UnsupportedOperationException.

Parameters
boolean hasStableIds

Whether items in data set have unique identifiers or not.

Throws
kotlin.UnsupportedOperationException

Always thrown, since this is unsupported by PagingDataAdapter.

setStateRestorationPolicy

public void setStateRestorationPolicy(
    @NonNull RecyclerView.Adapter.StateRestorationPolicy strategy
)

Sets the state restoration strategy for the Adapter. By default, it is set to ALLOW which means RecyclerView expects any set Adapter to be immediately capable of restoring the RecyclerView's saved scroll position.

This behaviour might be undesired if the Adapter's data is loaded asynchronously, and thus unavailable during initial layout (e.g. after Activity rotation). To avoid losing scroll position, you can change this to be either PREVENT_WHEN_EMPTY or PREVENT. Note that the former means your RecyclerView will restore state as soon as Adapter has 1 or more items while the latter requires you to call setStateRestorationPolicy with either ALLOW or PREVENT_WHEN_EMPTY again when the Adapter is ready to restore its state.

RecyclerView will still layout even when State restoration is disabled. The behavior of how State is restored is up to the LayoutManager. All default LayoutManagers will override current state with restored state when state restoration happens (unless an explicit call to scrollToPosition is made).

Calling this method after state is restored will not have any effect other than changing the return value of getStateRestorationPolicy.

Parameters
@NonNull RecyclerView.Adapter.StateRestorationPolicy strategy

The saved state restoration strategy for this Adapter.

snapshot

Added in 3.0.0
public final @NonNull ItemSnapshotList<@NonNull T> snapshot()

Returns a new ItemSnapshotList representing the currently presented items, including any placeholders if they are enabled.

submitData

Added in 3.0.0
public final void submitData(@NonNull PagingData<@NonNull T> pagingData)

Present a PagingData until it is invalidated by a call to refresh or PagingSource.invalidate.

This method is typically used when collecting from a Flow produced by Pager. For RxJava or LiveData support, use the non-suspending overload of submitData, which accepts a Lifecycle.

Note: This method suspends while it is actively presenting page loads from a PagingData, until the PagingData is invalidated. Although cancellation will propagate to this call automatically, collecting from a Pager.flow with the intention of presenting the most up-to-date representation of your backing dataset should typically be done using collectLatest.

import androidx.activity.viewModels

class MyFlowActivity : AppCompatActivity() {
    val pagingAdapter = UserPagingAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel by viewModels<UserListViewModel>()

        lifecycleScope.launch {
            viewModel.pagingFlow
                .collectLatest { pagingData ->
                    // submitData suspends until loading this generation of data stops
                    // so be sure to use collectLatest {} when presenting a Flow<PagingData>
                    pagingAdapter.submitData(pagingData)
                }
        }
    }
}
See also
Pager

submitData

Added in 3.0.0
public final void submitData(
    @NonNull Lifecycle lifecycle,
    @NonNull PagingData<@NonNull T> pagingData
)

Present a PagingData until it is either invalidated or another call to submitData is made.

This method is typically used when observing a RxJava or LiveData stream produced by Pager. For Flow support, use the suspending overload of submitData, which automates cancellation via CoroutineScope instead of relying of Lifecycle.

import androidx.activity.viewModels

class MyLiveDataActivity : AppCompatActivity() {
    val pagingAdapter = UserPagingAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel by viewModels<UserListViewModel>()

        viewModel.pagingLiveData.observe(this) { pagingData ->
            pagingAdapter.submitData(lifecycle, pagingData)
        }
    }
}
import androidx.activity.viewModels

class MyRxJava2Activity : AppCompatActivity() {
    val pagingAdapter = UserPagingAdapter()
    val disposable = CompositeDisposable()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel by viewModels<UserListViewModel>()

        viewModel.pagingFlowable
            .autoDispose(this) // Using AutoDispose to handle subscription lifecycle
            .subscribe { pagingData ->
                pagingAdapter.submitData(lifecycle, pagingData)
            }
    }
}
See also
submitData
Pager

withLoadStateFooter

Added in 3.0.0
public final @NonNull ConcatAdapter withLoadStateFooter(@NonNull LoadStateAdapter<@NonNull ?> footer)

Create a ConcatAdapter with the provided LoadStateAdapters displaying the LoadType.APPEND as a list item at the start of the presented list.

withLoadStateHeader

Added in 3.0.0
public final @NonNull ConcatAdapter withLoadStateHeader(@NonNull LoadStateAdapter<@NonNull ?> header)

Create a ConcatAdapter with the provided LoadStateAdapters displaying the LoadType.PREPEND as a list item at the end of the presented list.

withLoadStateHeaderAndFooter

Added in 3.0.0
public final @NonNull ConcatAdapter withLoadStateHeaderAndFooter(
    @NonNull LoadStateAdapter<@NonNull ?> header,
    @NonNull LoadStateAdapter<@NonNull ?> footer
)

Create a ConcatAdapter with the provided LoadStateAdapters displaying the LoadType.PREPEND and LoadType.APPENDs as list items at the start and end respectively.

Protected methods

getItem

Added in 3.0.0
@MainThread
protected final T getItem(@IntRange(from = 0) int position)

Returns the presented item at the specified position, notifying Paging of the item access to trigger any loads necessary to fulfill prefetchDistance.

Parameters
@IntRange(from = 0) int position

Index of the presented item to return, including placeholders.

Returns
T

The presented item at position, null if it is a placeholder