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 RecyclerView
.
Cómo definir una fuente de datos
El primer paso es definir una implementación de PagingSource
a fin de 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 con las corrutinas de Kotlin para la carga asíncrona. La biblioteca de Paging también proporciona clases para admitir otros frameworks asíncronos:
- Para usar RxJava, implementa
RxPagingSource
. - Si quieres usar
ListenableFuture
en Guava, implementaListenableFuturePagingSource
.
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 usado 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, deberás seleccionar Int
como el tipo de Key
y User
como el tipo de 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
.
Kotlin
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { 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 = response.nextPageNumber ) } catch (e: Exception) { // Handle errors in this block and return LoadResult.Error if it is an // expected error (such as a network failure). } } }
Java
class ExamplePagingSource extends RxPagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; ExamplePagingSource(@NonNull ExampleBackendService backend, @NonNull String query) { mBackend = backend; mQuery = query; } @NotNull @Override public Single<LoadResult<Integer, User>> loadSingle( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } return mBackend.searchUsers(mQuery, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUsersResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } }
Java
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; @NonNull private Executor mBgExecutor; ExamplePagingSource( @NonNull ExampleBackendService backend, @NonNull String query, @NonNull Executor bgExecutor) { mBackend = backend; mQuery = query; mBgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<LoadResult<Integer, User>> loadFuture( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( mBackend.searchUsers(mQuery, nextPageNumber), this::toLoadResult, mBgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, mBgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, mBgExecutor); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUsersResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } }
Una implementación típica de PagingSource
pasa parámetros proporcionados en su constructor al método load()
a fin de 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.
El objeto LoadResult
contiene el resultado de la operación de carga. LoadResult
es una clase sellada que toma una de dos posibles formas en virtud de si la llamada a load()
se realizó correctamente o no:
- Si la carga se realizó correctamente, muestra un objeto
LoadResult.Page
. - Si no se realizó correctamente, muestra un objeto
LoadResult.Error
.
En la figura 1, se ilustra cómo la función load()
de este ejemplo recibe la clave de cada carga y proporciona aquella para la carga posterior.
load()
usa y actualiza la clave
Cómo solucionar errores
Las solicitudes de carga de datos pueden fallar por varias razones, en especial cuando la carga se realiza a través de la red. Para informar errores detectados durante la carga, muestra 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()
:
Kotlin
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) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( backend.searchUsers(query, nextPageNumber), this::toLoadResult, bgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, bgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, bgExecutor);
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 a fin de que puedas realizar acciones sobre ellos. Si quieres obtener más información para exponer el estado de carga de la IU, consulta Cómo mostrar el estado de carga.
Cómo configurar el flujo de PagingData
A continuación, necesitas un flujo de datos paginados desde la implementación de PagingSource
.
Por lo general, debes configurar 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 admite el uso de varios tipos de flujo, incluidos Flow
, LiveData
y los tipos Flowable
y Observable
de RxJava.
Cuando creas una instancia de Pager
a fin de 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
:
Kotlin
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager); PagingRx.cachedIn(flowable, viewModelScope);
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
El operador cachedIn()
permite que el flujo de datos se pueda compartir y almacena en caché los datos cargados con el CoroutineScope
proporcionado. En este ejemplo, se usa el viewModelScope
que proporciona el artefacto lifecycle-viewmodel-ktx
de Lifecycle.
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.
Cómo definir un adaptador RecyclerView
También debes configurar un adaptador para recibir los datos en tu lista RecyclerView
. A tal fin, la biblioteca de Paging proporciona la clase PagingDataAdapter
.
Define una clase que extienda PagingDataAdapter
. En el ejemplo, UserAdapter
extiende PagingDataAdapter
a fin de proporcionar un adaptador RecyclerView
para los elementos de lista de tipo User
y usa UserViewHolder
como contenedor de vistas:
Kotlin
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : PagingDataAdapter<User, UserViewHolder>(diffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): UserViewHolder { return UserViewHolder(parent) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val item = getItem(position) // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item) } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
El adaptador también debe definir los métodos onCreateViewHolder()
y onBindViewHolder()
, y especificar una DiffUtil.ItemCallback
.
Esto funciona como lo hace normalmente cuando defines los adaptadores de lista RecyclerView
:
Kotlin
object UserComparator : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // Id is unique. return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Cómo mostrar los datos paginados en tu IU
Ahora que definiste un PagingSource
, creaste una forma para que tu app genere un flujo de PagingData
y definiste un PagingDataAdapter
, está todo listo para que conectes estos elementos y muestres los datos paginados en tu actividad.
Realiza los siguientes pasos en el método onCreate
de la actividad o en el onViewCreated
del fragmento:
- Crea una instancia de tu clase
PagingDataAdapter
. - Pasa la instancia
PagingDataAdapter
a la listaRecyclerView
que deseas que muestre tus datos paginados. - Observa el flujo de
PagingData
y pasa cada valor generado al métodosubmitData()
del adaptador.
Kotlin
val viewModel by viewModels<ExampleViewModel>() val pagingAdapter = UserAdapter(UserComparator) val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.adapter = pagingAdapter // Activities can use lifecycleScope directly, but Fragments should instead use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } }
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter viewModel.flowable // Using AutoDispose to handle subscription lifecycle. // See: https://github.com/uber/AutoDispose .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter // Activities can use getLifecycle() directly, but Fragments should instead use // getViewLifecycleOwner().getLifecycle(). viewModel.liveData.observe(this, pagingData -> pagingAdapter.submitData(getLifecycle(), pagingData));
La lista RecyclerView
ahora muestra los datos paginados de la fuente de datos y carga automáticamente otra página cuando es necesario.
Cómo mostrar el estado de carga
La biblioteca de Paging expone el estado de carga para usar en la IU mediante el objeto LoadState
. LoadState
toma una de tres formas según el estado de carga actual:
- Si no hay una operación de carga activa ni errores,
LoadState
es un objetoLoadState.NotLoading
. - Si hay una operación de carga activa,
LoadState
es un objetoLoadState.Loading
. - Si hay un error,
LoadState
es un objetoLoadState.Error
.
Existen dos maneras de usar LoadState
en tu IU: con un objeto de escucha o con un adaptador de lista especial a fin de presentar el estado de carga directamente en la lista RecyclerView
.
Cómo usar un objeto de escucha para obtener el estado de carga
A los efectos de obtener el estado de carga para su uso general en la IU, PagingDataAdapter
incluye el método addLoadStateListener()
.
Kotlin
// Activities can use lifecycleScope directly, but Fragments should instead use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { pagingAdapter.loadStateFlow.collectLatest { loadStates -> progressBar.isVisible = loadStates.refresh is LoadState.Loading retry.isVisible = loadState.refresh !is LoadState.Loading errorMsg.isVisible = loadState.refresh is LoadState.Error } }
Java
pagingAdapter.addLoadStateListener(loadStates -> { progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.VISIBLE : View.GONE); retry.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.GONE : View.VISIBLE); errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error ? View.VISIBLE : View.GONE); });
Java
pagingAdapter.addLoadStateListener(loadStates -> { progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.VISIBLE : View.GONE); retry.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.GONE : View.VISIBLE); errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error ? View.VISIBLE : View.GONE); });
Cómo usar un adaptador para presentar el estado de carga
La biblioteca de Paging proporciona otro adaptador de lista llamado LoadStateAdapter
con el fin de presentar el estado de carga directamente en la lista de datos paginados que se muestra.
Primero, crea una clase que implemente LoadStateAdapter
y define los métodos onCreateViewHolder()
y onBindViewHolder()
:
Kotlin
class LoadStateViewHolder( parent: ViewGroup, retry: () -> Unit ) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.load_state_item, parent, false) ) { private val binding = LoadStateItemBinding.bind(itemView) private val progressBar: ProgressBar = binding.progressBar private val errorMsg: TextView = binding.errorMsg private val retry: Button = binding.retryButton .also { it.setOnClickListener { retry() } } fun bind(loadState: LoadState) { if (loadState is LoadState.Error) { errorMsg.text = loadState.error.localizedMessage } progressBar.isVisible = loadState is LoadState.Loading retry.isVisible = loadState is LoadState.Error errorMsg.isVisible = loadState is LoadState.Error } } // Adapter that displays a loading spinner when // state = LoadState.Loading, and an error message and retry // button when state is LoadState.Error. class ExampleLoadStateAdapter( private val retry: () -> Unit ) : LoadStateAdapter<LoadStateViewHolder()> { override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = LoadStateViewHolder(parent, retry) override fun onBindViewHolder( holder: LoadStateViewHolder, loadState: LoadState ) = holder.bind(loadState) }
Java
class LoadStateViewHolder extends RecyclerView.ViewHolder { private ProgressBar mProgressBar; private TextView mErrorMsg; private Button mRetry; LoadStateViewHolder( @NonNull ViewGroup parent, @NonNull View.OnClickListener retryCallback) { super(LayoutInflater.from(parent.getContext()) .inflate(R.layout.load_state_item, parent, false)); LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView); mProgressBar = binding.progressBar; mErrorMsg = binding.errorMsg; mRetry = binding.retryButton; } public void bind(LoadState loadState) { if (loadState instanceof LoadState.Error) { LoadState.Error loadStateError = (LoadState.Error) loadState; mErrorMsg.setText(loadStateError.getError().getLocalizedMessage()); } mProgressBar.setVisibility(loadState instanceof LoadState.Loading ? View.VISIBLE : View.GONE); mRetry.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); mErrorMsg.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); } } // Adapter that displays a loading spinner when // state instanceOf LoadState.Loading, and an error message and // retry button when state instanceof LoadState.Error. class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> { private View.OnClickListener mRetryCallback; ExampleLoadStateAdapter(View.OnClickListener retryCallback) { mRetryCallback = retryCallback; } @NotNull @Override public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent, @NotNull LoadState loadState) { return new LoadStateViewHolder(parent, mRetryCallback); } @Override public void onBindViewHolder(@NotNull LoadStateViewHolder holder, @NotNull LoadState loadState) { holder.bind(loadState); } }
Java
class LoadStateViewHolder extends RecyclerView.ViewHolder { private ProgressBar mProgressBar; private TextView mErrorMsg; private Button mRetry; LoadStateViewHolder( @NonNull ViewGroup parent, @NonNull View.OnClickListener retryCallback) { super(LayoutInflater.from(parent.getContext()) .inflate(R.layout.load_state_item, parent, false)); LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView); mProgressBar = binding.progressBar; mErrorMsg = binding.errorMsg; mRetry = binding.retryButton; } public void bind(LoadState loadState) { if (loadState instanceof LoadState.Error) { LoadState.Error loadStateError = (LoadState.Error) loadState; mErrorMsg.setText(loadStateError.getError().getLocalizedMessage()); } mProgressBar.setVisibility(loadState instanceof LoadState.Loading ? View.VISIBLE : View.GONE); mRetry.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); mErrorMsg.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); } } // Adapter that displays a loading spinner when // state instanceOf LoadState.Loading, and an error message and // retry button when state instanceof LoadState.Error. class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> { private View.OnClickListener mRetryCallback; ExampleLoadStateAdapter(View.OnClickListener retryCallback) { mRetryCallback = retryCallback; } @NotNull @Override public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent, @NotNull LoadState loadState) { return new LoadStateViewHolder(parent, mRetryCallback); } @Override public void onBindViewHolder(@NotNull LoadStateViewHolder holder, @NotNull LoadState loadState) { holder.bind(loadState); } }
Luego, llama al método withLoadStateHeaderAndFooter()
desde el objeto PagingDataAdapter
:
Kotlin
pagingAdapter .withLoadStateHeaderAndFooter( header = ExampleLoadStateAdapter(adapter::retry), footer = ExampleLoadStateAdapter(adapter::retry) )
Java
pagingAdapter .withLoadStateHeaderAndFooter( new ExampleLoadStateAdapter(pagingAdapter::retry), new ExampleLoadStateAdapter(pagingAdapter::retry));
Java
pagingAdapter .withLoadStateHeaderAndFooter( new ExampleLoadStateAdapter(pagingAdapter::retry), new ExampleLoadStateAdapter(pagingAdapter::retry));
En cambio, puedes llamar a withLoadStateHeader()
o withLoadStateFooter()
si solo quieres que RecyclerView
muestre el estado de carga en el encabezado o pie de página.
Recursos adicionales
Para obtener más información sobre la biblioteca de Paging, consulta los siguientes recursos adicionales: