La biblioteca de Paging proporciona capacidades potentes para cargar y mostrar datos paginados desde un conjunto de datos más grande. En esta guía, se muestra cómo usar la biblioteca de Paging para configurar un flujo de datos paginados de una fuente de datos de red y mostrarlos en una lista diferida.
Cómo definir una fuente de datos
El primer paso es definir una implementación de PagingSource para identificar la fuente de datos. La clase de API PagingSource incluye el método load, que debes anular para indicar cómo recuperar datos paginados de la fuente de datos correspondiente.
Usa la clase PagingSource directamente para utilizar las corrutinas de Kotlin para la carga asíncrona.
Cómo seleccionar tipos de clave y de valor
PagingSource<Key, Value> tiene dos parámetros de tipo: Key y Value. La clave define el identificador que se usa para cargar los datos, y el valor es el tipo de los datos en sí. Por ejemplo, si cargas páginas de objetos User desde la red pasando números de página Int a Retrofit, selecciona Int como el tipo Key y User como el tipo Value.
Cómo definir PagingSource
En el siguiente ejemplo, se implementa un PagingSource que carga páginas de elementos por número de página. El tipo de Key es Int y el tipo de Value es User.
class ExamplePagingSource(
val backend: ExampleBackendService,
val query: String
) : PagingSource<Int, User>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, User> {
init {
// the data source is expected to be immutable
// invalidate PagingSource if data source
// has updated
backEnd.addDatabaseOnChangedListener {
invalidate()
}
}
try {
// Start refresh at page 1 if undefined.
val nextPageNumber = params.key ?: 1
val response = backend.searchUsers(query, nextPageNumber)
return LoadResult.Page(
data = response.users,
prevKey = null, // Only paging forward.
nextKey = nextPageNumber + 1
)
} catch (e: Exception) {
// Handle errors in this block and return LoadResult.Error for
// expected errors (such as a network failure).
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
// Try to find the page key of the closest page to anchorPosition from
// either the prevKey or the nextKey; you need to handle nullability
// here.
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey are null -> anchorPage is the
// initial page, so return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
Una implementación típica de PagingSource pasa parámetros proporcionados en su constructor al método load para cargar los datos apropiados para una búsqueda. En el ejemplo anterior, esos parámetros son los siguientes:
backend: Instancia del servicio de backend que proporciona los datosquery: La búsqueda para enviar al servicio indicado porbackend
El objeto LoadParams contiene información sobre la operación de carga que se realizará. Esto incluye la clave y la cantidad de elementos que se cargarán. Para diferenciar entre los tipos de carga en load, verifica qué subclase de LoadParams se pasó: LoadParams.Refresh, LoadParams.Append o LoadParams.Prepend.
El objeto LoadResult contiene el resultado de la operación de carga. LoadResult es una clase sellada
que toma una de tres posibles formas en virtud de si la llamada a load se realizó correctamente o no:
- Si la carga se realizó correctamente, devuelve un objeto
LoadResult.Page. PasanullparanextKeyoprevKeysi no se pueden cargar más datos en esa dirección. - Si no se realizó correctamente, devuelve un objeto
LoadResult.Error. - Si el
PagingSourceya no es válido porque cambiaron los datos subyacentes, devuelve un objetoLoadResult.Invalid.
En la siguiente figura, se ilustra el modo en que la función load de este ejemplo recibe la clave para cada carga y proporciona la clave de la carga posterior.
load usa y actualiza la clave
La implementación de PagingSource también debe implementar un método getRefreshKey que tome un objeto PagingState como parámetro. Devuelve la clave para pasar al método load cuando los datos se actualizan o se invalidan después de la carga inicial. La biblioteca de Paging llama a este método automáticamente en las actualizaciones posteriores de los datos. Para la paginación basada en compensaciones en la que las claves representan las posiciones de los elementos, puedes calcular la clave de actualización en relación con state.anchorPosition:
// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
// Center the refreshed load around the user's current viewport so the list
// does not jump back to the top on invalidation.
return state.anchorPosition?.let { anchorPosition ->
(anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
}
}
Soluciona errores
Las solicitudes de carga de datos pueden fallar por varias razones, en especial cuando la carga se realiza a través de una red. Para informar errores detectados durante la carga, devuelve un objeto LoadResult.Error desde el método load.
Por ejemplo, puedes detectar e informar errores de carga en ExamplePagingSource del ejemplo anterior si agregas lo siguiente al método load:
catch (e: IOException) {
// IOException for network failures.
return LoadResult.Error(e)
} catch (e: HttpException) {
// HttpException for any non-2xx HTTP status codes.
return LoadResult.Error(e)
}
Para obtener más información sobre la solución de errores de Retrofit, consulta los ejemplos en la referencia de la API de PagingSource.
PagingSource recopila y entrega objetos LoadResult.Error a la IU para que puedas realizar acciones sobre ellos. Si deseas obtener más información para exponer el estado de carga
en la IU, consulta Cómo administrar y presentar estados de carga.
Cómo configurar el flujo de PagingData
A continuación, necesitas un flujo de datos paginados desde la implementación de PagingSource.
Configura el flujo de datos en tu ViewModel. La clase Pager proporciona métodos que exponen un flujo reactivo de objetos PagingData desde PagingSource. La biblioteca de Paging expone el flujo de datos como un Flow.
Cuando creas una instancia de Pager para configurar tu flujo reactivo, debes proporcionarle un objeto de configuración PagingConfig y una función que le indique a Pager cómo obtener una instancia de tu implementación de PagingSource. También puedes especificar un initialKey opcional, como 1 para
números de página basados en 1 o 0 para la paginación basada en compensación. En PagingConfig, puedes
configurar pageSize, initialLoadSize, prefetchDistance y
enablePlaceholders. La propiedad prefetchDistance define cuántos elementos de cualquier borde del contenido cargado activan la siguiente recuperación.
class UserViewModel(
private val backend: ExampleBackendService,
private val query: String
) : ViewModel() {
val userPagingFlow: Flow<PagingData<User>> = Pager(
config = PagingConfig(
pageSize = 20,
// Load 3 pages initially so the user can scroll immediately without
// hitting a loading state.
initialLoadSize = 60,
// Fetch the next page far enough ahead of the scroll position to
// avoid visible pauses.
prefetchDistance = 20,
enablePlaceholders = true
),
initialKey = 1,
pagingSourceFactory = {
ExamplePagingSource(backend, query)
}
)
.flow
.cachedIn(viewModelScope)
}
El operador cachedIn permite que el flujo de datos se pueda compartir y almacena en caché los datos cargados con el CoroutineScope proporcionado. Sin cachedIn, no se puede
volver a recopilar el PagingData. En este ejemplo, se usa el viewModelScope que proporciona el artefacto lifecycle-viewmodel-ktx del ciclo de vida.
El objeto Pager llama al método load desde el objeto PagingSource, lo cual hace que le proporcione el objeto LoadParams y que reciba el objeto LoadResult a cambio.
Recopila y muestra los datos en tu IU
Para conectar el flujo paginado a la IU, obtén el flujo de tu ViewModel y pásalo a tu lista de elementos de componibilidad.
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val userFlow = viewModel.userPagingFlow
UserList(flow = userFlow)
}
Usa collectAsLazyPagingItems para convertir el flujo PagingData en LazyPagingItems. Luego, usa la API de items dentro de un LazyColumn para diseñar cada elemento.
Asegúrate de proporcionar un identificador único y estable para cada elemento con itemKey.
En el siguiente ejemplo, se usa it.id (que hace referencia a la propiedad User.id) porque
se mantiene estable para la instancia de User en las actualizaciones de datos.
@Composable
fun UserList(flow: Flow<PagingData<User>>) {
val lazyPagingItems = flow.collectAsLazyPagingItems()
LazyColumn {
items(
lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey { it.id }
) { index ->
val user = lazyPagingItems[index]
if (user != null) {
UserRow(user)
} else {
UserPlaceholder()
}
}
}
}
La biblioteca de Paging usa null para los marcadores de posición mientras se carga una página, por lo que, si habilitaste los marcadores de posición, debes controlar los valores null en el bloque de contenido.
Ahora, la lista muestra los datos paginados, y la biblioteca de Paging carga páginas adicionales a medida que el usuario se desplaza.
Recursos adicionales
Para obtener más información sobre la biblioteca de Paging, consulta los siguientes recursos adicionales:
Documentación
Contenido de vistas
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Página de la red y la base de datos
- Cómo migrar a Paging 3
- Descripción general de la biblioteca de Paging