La biblioteca de Paging hace un seguimiento del estado de las solicitudes de carga de los datos paginados y lo muestra a través de la clase LoadState
.
Tu app puede registrar un objeto de escucha con PagingDataAdapter
para recibir información sobre el estado actual y actualizar la IU según corresponda. Estos estados provienen del adaptador, ya que son síncronos con la IU.
Esto significa que el objeto de escucha recibe actualizaciones cuando se aplica la carga de la página la IU.
Se proporciona un indicador independiente de LoadState
para cada LoadType
y tipo de fuente de datos (PagingSource
o RemoteMediator
). El objeto CombinedLoadStates
, que proviene del objeto de escucha, proporciona información sobre el estado de carga de todos estos indicadores. Puedes usar esta información detallada para mostrar los indicadores de carga correspondientes a tus usuarios.
Estados de carga
La biblioteca de Paging expone el estado de carga para usar en la IU mediante el objeto LoadState
. Los objetos LoadState
toman una de las siguientes tres formas según el estado de carga actual:
- Si no hay una operación de carga activa ni un error,
LoadState
es un objetoLoadState.NotLoading
. Esta subclase también incluye la propiedadendOfPaginationReached
, que indica si se completó la paginación. - 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 para presentar el estado de carga directamente en la lista RecyclerView
.
Cómo acceder al estado de carga mediante un objeto de escucha
Para obtener el estado de carga para su uso general en la IU, usa el flujo de loadStateFlow
o el método addLoadStateListener()
proporcionado por tu PagingDataAdapter
. Estos mecanismos brindan acceso a un objeto CombinedLoadStates
, que incluye información sobre el comportamiento de LoadState
para cada tipo de carga.
En el siguiente ejemplo, PagingDataAdapter
muestra los distintos componentes de la IU en función del estado de carga actual de la actualización:
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); });
Para obtener más información sobre CombinedLoadStates
, consulta Cómo acceder a información adicional del estado de carga.
Cómo presentar el estado de carga con un adaptador
La biblioteca de Paging proporciona otro adaptador de lista llamado LoadStateAdapter
para presentar el estado de carga directamente en la lista de datos paginados que se muestra. Este adaptador brinda acceso al estado de carga actual de la lista, la cual puedes pasar a un contenedor de vistas personalizadas que muestre la información.
Primero, crea una clase de contenedor de vistas que conserve referencias a las vistas de error y de carga de tu pantalla. Crea una función bind()
que acepte un LoadState
como parámetro. Esta función debería activar o desactivar la visibilidad de la vista según el parámetro del estado de carga:
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 } }
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); } }
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); } }
A continuación, crea una clase que implemente LoadStateAdapter
y define los métodos onCreateViewHolder()
y onBindViewHolder()
. Estos métodos crean una instancia de tu contenedor de vistas personalizadas y vinculan el estado de carga asociado.
Kotlin
// Adapter that displays a loading spinner when // state is 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
// Adapter that displays a loading spinner when // state is LoadState.Loading, and an error message and retry // button when state is 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
// Adapter that displays a loading spinner when // state is LoadState.Loading, and an error message and retry // button when state is 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); } }
Cómo mostrar el estado de carga como un encabezado o pie de página
Para mostrar el progreso de carga en un encabezado y un pie de página, llama al método withLoadStateHeaderAndFooter()
de tu objeto PagingDataAdapter
de la siguiente manera:
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));
Si quieres que RecyclerView
muestre el estado de carga únicamente en el encabezado o en el pie de página, puedes llamar a withLoadStateHeader()
o withLoadStateFooter()
.
Cómo acceder a información adicional sobre el estado de carga
El objeto CombinedLoadStates
de PagingDataAdapter
proporciona información sobre los estados de carga de tu implementación de PagingSource
y, también, de RemoteMediator
(si la hay).
Usa las propiedades refresh
, append
y prepend
de CombinedLoadStates
para acceder al objeto LoadState
del tipo de carga correspondiente para mayor comodidad. Por lo general, estas propiedades derivan al estado de carga de la implementación de RemoteMediator
(si existe). De lo contrario, contienen el estado de carga correspondiente de la implementación de PagingSource
. Para obtener información más detallada sobre la lógica subyacente, consulta la documentación de referencia de CombinedLoadStates
.
Kotlin
lifecycleScope.launch { pagingAdapter.loadStateFlow.collectLatest { loadStates -> // Observe refresh load state from RemoteMediator if present, or // from PagingSource otherwise. refreshLoadState: LoadState = loadStates.refresh // Observe prepend load state from RemoteMediator if present, or // from PagingSource otherwise. prependLoadState: LoadState = loadStates.prepend // Observe append load state from RemoteMediator if present, or // from PagingSource otherwise. appendLoadState: LoadState = loadStates.append } }
Java
pagingAdapter.addLoadStateListener(loadStates -> { // Observe refresh load state from RemoteMediator if present, or // from PagingSource otherwise. LoadState refreshLoadState = loadStates.refresh; // Observe prepend load state from RemoteMediator if present, or // from PagingSource otherwise. LoadState prependLoadState = loadStates.prepend; // Observe append load state from RemoteMediator if present, or // from PagingSource otherwise. LoadState appendLoadState = loadStates.append; });
Java
pagingAdapter.addLoadStateListener(loadStates -> { // Observe refresh load state from RemoteMediator if present, or // from PagingSource otherwise. LoadState refreshLoadState = loadStates.refresh; // Observe prepend load state from RemoteMediator if present, or // from PagingSource otherwise. LoadState prependLoadState = loadStates.prepend; // Observe append load state from RemoteMediator if present, or // from PagingSource otherwise. LoadState appendLoadState = loadStates.append; });
Sin embargo, es importante tener en cuenta que solo se garantiza que los estados de carga de PagingSource
sean síncronos con actualizaciones de la IU. Dado que es posible que las propiedades refresh
, append
y prepend
tomen el estado de carga de PagingSource
o RemoteMediator
, no se puede garantizar que sean síncronas con las actualizaciones de la IU. Esto ocasiona problemas en la IU, por los que la carga parece completarse antes de que se agreguen datos nuevos a la IU.
Por este motivo, los descriptores de acceso son útiles para presentar el estado de carga en un encabezado o un pie de página. Sin embargo, para otros casos de uso, es posible que necesites acceder específicamente al estado de carga desde PagingSource
o RemoteMediator
. A tal fin, CombinedLoadStates
proporciona las propiedades source
y mediator
. Cada una de estas propiedades expone un objeto LoadStates
que contiene los objetos LoadState
de PagingSource
o RemoteMediator
, respectivamente:
Kotlin
lifecycleScope.launch { pagingAdapter.loadStateFlow.collectLatest { loadStates -> // Directly access the RemoteMediator refresh load state. mediatorRefreshLoadState: LoadState? = loadStates.mediator.refresh // Directly access the RemoteMediator append load state. mediatorAppendLoadState: LoadState? = loadStates.mediator.append // Directly access the RemoteMediator prepend load state. mediatorPrependLoadState: LoadState? = loadStates.mediator.prepend // Directly access the PagingSource refresh load state. sourceRefreshLoadState: LoadState = loadStates.source.refresh // Directly access the PagingSource append load state. sourceAppendLoadState: LoadState = loadStates.source.append // Directly access the PagingSource prepend load state. sourcePrependLoadState: LoadState = loadStates.source.prepend } }
Java
pagingAdapter.addLoadStateListener(loadStates -> { // Directly access the RemoteMediator refresh load state. LoadState mediatorRefreshLoadState = loadStates.mediator.refresh; // Directly access the RemoteMediator append load state. LoadState mediatorAppendLoadState = loadStates.mediator.append; // Directly access the RemoteMediator prepend load state. LoadState mediatorPrependLoadState = loadStates.mediator.prepend; // Directly access the PagingSource refresh load state. LoadState sourceRefreshLoadState = loadStates.source.refresh; // Directly access the PagingSource append load state. LoadState sourceAppendLoadState = loadStates.source.append; // Directly access the PagingSource prepend load state. LoadState sourcePrependLoadState = loadStates.source.prepend; });
Java
pagingAdapter.addLoadStateListener(loadStates -> { // Directly access the RemoteMediator refresh load state. LoadState mediatorRefreshLoadState = loadStates.mediator.refresh; // Directly access the RemoteMediator append load state. LoadState mediatorAppendLoadState = loadStates.mediator.append; // Directly access the RemoteMediator prepend load state. LoadState mediatorPrependLoadState = loadStates.mediator.prepend; // Directly access the PagingSource refresh load state. LoadState sourceRefreshLoadState = loadStates.source.refresh; // Directly access the PagingSource append load state. LoadState sourceAppendLoadState = loadStates.source.append; // Directly access the PagingSource prepend load state. LoadState sourcePrependLoadState = loadStates.source.prepend; });
Operadores de cadena de LoadState
Debido a que el objeto CombinedLoadStates
proporciona acceso a cada cambio en el estado de carga, es importante filtrar el flujo de estados de carga en función de eventos específicos. Esto garantiza que actualices tu IU en el momento adecuado para evitar interrupciones y actualizaciones innecesarias de la IU.
Por ejemplo, supongamos que deseas mostrar una vista vacía, pero solo después de que se complete la carga inicial de datos. En este caso de uso, debes verificar que se haya iniciado una carga de actualización de datos y, luego, esperar a que el estado NotLoading
confirme que se completó la actualización. Debes filtrar todos los indicadores, excepto los que necesitas:
Kotlin
lifecycleScope.launchWhenCreated { adapter.loadStateFlow // Only emit when REFRESH LoadState for RemoteMediator changes. .distinctUntilChangedBy { it.refresh } // Only react to cases where REFRESH completes, such as NotLoading. .filter { it.refresh is LoadState.NotLoading } // Scroll to top is synchronous with UI updates, even if remote load was // triggered. .collect { binding.list.scrollToPosition(0) } }
Java
PublishSubject<CombinedLoadStates> subject = PublishSubject.create(); Disposable disposable = subject.distinctUntilChanged(CombinedLoadStates::getRefresh) .filter( combinedLoadStates -> combinedLoadStates.getRefresh() instanceof LoadState.NotLoading) .subscribe(combinedLoadStates -> binding.list.scrollToPosition(0)); pagingAdapter.addLoadStateListener(loadStates -> { subject.onNext(loadStates); });
Java
LiveData<CombinedLoadStates> liveData = new MutableLiveData<>(); LiveData<LoadState> refreshLiveData = Transformations.map(liveData, CombinedLoadStates::getRefresh); LiveData<LoadState> distinctLiveData = Transformations.distinctUntilChanged(refreshLiveData); distinctLiveData.observeForever(loadState -> { if (loadState instanceof LoadState.NotLoading) { binding.list.scrollToPosition(0); } });
En este ejemplo, se espera hasta que se actualice el estado de carga de actualización, pero solo se activa cuando el estado es NotLoading
. Esto garantiza que la actualización remota se complete antes de que se actualice la IU.
Las API de flujo hacen posible este tipo de operación. Tu app puede especificar los eventos de carga que necesita y controlar los datos nuevos si se cumplen los criterios correspondientes.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo cargar y mostrar datos paginados
- Página de la red y la base de datos
- Descripción general de la biblioteca de Paging