RemoteMediator


Known direct subclasses
ListenableFutureRemoteMediator

ListenableFuture-based compatibility wrapper around RemoteMediator's suspending APIs.

RxRemoteMediator

RxJava2 compatibility wrapper around RemoteMediator's suspending APIs.

RxRemoteMediator

RxJava3 compatibility wrapper around RemoteMediator's suspending APIs.


Defines a set of callbacks used to incrementally load data from a remote source into a local source wrapped by a PagingSource, e.g., loading data from network into a local db cache.

A RemoteMediator is registered by passing it to Pager's constructor.

RemoteMediator allows hooking into the following events:

import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.paging.samples.shared.ExampleBackendService
import androidx.paging.samples.shared.RoomDb
import androidx.paging.samples.shared.User
import androidx.room.withTransaction

/**
 * Sample RemoteMediator for a DB + Network based PagingData stream, which triggers network
 * requests to fetch additional items when a user scrolls to the end of the list of items stored
 * in DB.
 *
 * This sample loads a list of [User] items from an item-keyed Retrofit paginated source. This
 * source is "item-keyed" because we're loading the next page using information from the items
 * themselves (the ID param) as a key to fetch more data.
 */
@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
    private val query: String,
    private val database: RoomDb,
    private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
    val userDao = database.userDao()

    override suspend fun initialize(): InitializeAction {
        val cacheTimeout = TimeUnit.HOURS.convert(1, TimeUnit.MILLISECONDS)
        return if (System.currentTimeMillis() - userDao.lastUpdated() >= cacheTimeout) {
            // Cached data is up-to-date, so there is no need to re-fetch from network.
            InitializeAction.SKIP_INITIAL_REFRESH
        } else {
            // Need to refresh cached data from network; returning LAUNCH_INITIAL_REFRESH here
            // will also block RemoteMediator's APPEND and PREPEND from running until REFRESH
            // succeeds.
            InitializeAction.LAUNCH_INITIAL_REFRESH
        }
    }

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, User>
    ): MediatorResult {
        return try {
            // The network load method takes an optional `after=<user.id>` parameter. For every
            // page after the first, we pass the last user ID to let it continue from where it
            // left off. For REFRESH, pass `null` to load the first page.
            val loadKey = when (loadType) {
                LoadType.REFRESH -> null
                // In this example, we never need to prepend, since REFRESH will always load the
                // first page in the list. Immediately return, reporting end of pagination.
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
                LoadType.APPEND -> {
                    val lastItem = state.lastItemOrNull()

                    // We must explicitly check if the last item is `null` when appending,
                    // since passing `null` to networkService is only valid for initial load.
                    // If lastItem is `null` it means no items were loaded after the initial
                    // REFRESH and there are no more items to load.
                    if (lastItem == null) {
                        return MediatorResult.Success(endOfPaginationReached = true)
                    }

                    lastItem.id
                }
            }

            // Suspending network load via Retrofit. This doesn't need to be wrapped in a
            // withContext(Dispatcher.IO) { ... } block since Retrofit's Coroutine CallAdapter
            // dispatches on a worker thread.
            val response = networkService.searchUsers(query = query, after = loadKey)

            database.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    userDao.deleteByQuery(query)
                }

                // Insert new users into database, which invalidates the current
                // PagingData, allowing Paging to present the updates in the DB.
                userDao.insertAll(response.users)
            }

            MediatorResult.Success(endOfPaginationReached = response.nextKey == null)
        } catch (e: IOException) {
            MediatorResult.Error(e)
        } catch (e: HttpException) {
            MediatorResult.Error(e)
        }
    }
}
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.paging.samples.shared.ExampleBackendService
import androidx.paging.samples.shared.RemoteKey
import androidx.paging.samples.shared.RoomDb
import androidx.paging.samples.shared.User
import androidx.room.withTransaction

/**
 * Sample RemoteMediator for a DB + Network based PagingData stream, which triggers network
 * requests to fetch additional items when a user scrolls to the end of the list of items stored
 * in DB.
 *
 * This sample loads a list of [User] via Retrofit from a page-keyed network service using
 * [String] tokens to load pages (each response has a next/previous token), and inserts them
 * into database.
 */
@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
    private val query: String,
    private val database: RoomDb,
    private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
    val userDao = database.userDao()
    val remoteKeyDao = database.remoteKeyDao()

    override suspend fun initialize(): InitializeAction {
        val cacheTimeout = TimeUnit.HOURS.convert(1, TimeUnit.MILLISECONDS)
        return if (System.currentTimeMillis() - userDao.lastUpdated() >= cacheTimeout) {
            // Cached data is up-to-date, so there is no need to re-fetch from network.
            InitializeAction.SKIP_INITIAL_REFRESH
        } else {
            // Need to refresh cached data from network; returning LAUNCH_INITIAL_REFRESH here
            // will also block RemoteMediator's APPEND and PREPEND from running until REFRESH
            // succeeds.
            InitializeAction.LAUNCH_INITIAL_REFRESH
        }
    }

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, User>
    ): MediatorResult {
        return try {
            // The network load method takes an optional [String] parameter. For every page
            // after the first, we pass the [String] token returned from the previous page to
            // let it continue from where it left off. For REFRESH, pass `null` to load the
            // first page.
            val loadKey = when (loadType) {
                LoadType.REFRESH -> null
                // In this example, we never need to prepend, since REFRESH will always load the
                // first page in the list. Immediately return, reporting end of pagination.
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
                // Query remoteKeyDao for the next RemoteKey.
                LoadType.APPEND -> {
                    val remoteKey = database.withTransaction {
                        remoteKeyDao.remoteKeyByQuery(query)
                    }

                    // We must explicitly check if the page key is `null` when appending,
                    // since `null` is only valid for initial load. If we receive `null`
                    // for APPEND, that means we have reached the end of pagination and
                    // there are no more items to load.
                    if (remoteKey.nextKey == null) {
                        return MediatorResult.Success(endOfPaginationReached = true)
                    }

                    remoteKey.nextKey
                }
            }

            // Suspending network load via Retrofit. This doesn't need to be wrapped in a
            // withContext(Dispatcher.IO) { ... } block since Retrofit's Coroutine CallAdapter
            // dispatches on a worker thread.
            val response = networkService.searchUsers(query, loadKey)

            // Store loaded data, and next key in transaction, so that they're always consistent
            database.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    remoteKeyDao.deleteByQuery(query)
                    userDao.deleteByQuery(query)
                }

                // Update RemoteKey for this query.
                remoteKeyDao.insertOrReplace(RemoteKey(query, response.nextKey))

                // Insert new users into database, which invalidates the current
                // PagingData, allowing Paging to present the updates in the DB.
                userDao.insertAll(response.users)
            }

            MediatorResult.Success(endOfPaginationReached = response.nextKey == null)
        } catch (e: IOException) {
            MediatorResult.Error(e)
        } catch (e: HttpException) {
            MediatorResult.Error(e)
        }
    }
}

Summary

Nested types

Return type of initialize, which signals the action to take after initialize completes.

Return type of load, which determines LoadState.

Recoverable error that can be retried, sets the LoadState to LoadState.Error.

Success signaling that LoadState should be set to LoadState.NotLoading if endOfPaginationReached is true, otherwise LoadState is kept at LoadState.Loading to await invalidation.

Public constructors

<Key : Any, Value : Any> RemoteMediator()
Cmn

Public functions

open suspend RemoteMediator.InitializeAction

Callback fired during initialization of a PagingData stream, before initial load.

Cmn
abstract suspend RemoteMediator.MediatorResult
load(loadType: LoadType, state: PagingState<Key, Value>)

Callback triggered when Paging needs to request more data from a remote source due to any of the following events:

Cmn

Public constructors

RemoteMediator

<Key : Any, Value : Any> RemoteMediator()

Public functions

initialize

open suspend fun initialize(): RemoteMediator.InitializeAction

Callback fired during initialization of a PagingData stream, before initial load.

This function runs to completion before any loading is performed.

Returns
RemoteMediator.InitializeAction

InitializeAction used to control whether load with load type REFRESH will be immediately dispatched when the first PagingData is submitted:

load

abstract suspend fun load(loadType: LoadType, state: PagingState<Key, Value>): RemoteMediator.MediatorResult

Callback triggered when Paging needs to request more data from a remote source due to any of the following events:

It is the responsibility of this method to update the backing dataset and trigger PagingSource.invalidate to allow androidx.paging.PagingDataAdapter to pick up new items found by load.

The runtime and result of this method defines the remote LoadState behavior sent to the UI via CombinedLoadStates.

This method is never called concurrently unless Pager.flow has multiple collectors. Note that Paging might cancel calls to this function if it is currently executing a PREPEND or APPEND and a REFRESH is requested. In that case, REFRESH has higher priority and will be executed after the previous call is cancelled. If the load call with REFRESH returns an error, Paging will call load with the previously cancelled APPEND or PREPEND request. If REFRESH succeeds, it won't make the APPEND or PREPEND requests unless they are necessary again after the REFRESH is applied to the UI.

Parameters
loadType: LoadType

LoadType of the condition which triggered this callback.

state: PagingState<Key, Value>

A copy of the state including the list of pages currently held in memory of the currently presented PagingData at the time of starting the load. E.g. for load(loadType = APPEND), you can use the page or item at the end as input for what to load from the network.

Returns
RemoteMediator.MediatorResult

MediatorResult signifying what LoadState to be passed to the UI, and whether there's more data available.