Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Cómo cargar y mostrar datos paginados

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:

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 datos
  • query: la búsqueda para enviar al servicio indicado por backend

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.

En cada llamada load(), ExamplePagingSource toma la clave actual y muestra la siguiente que se cargará.
Figura 1: Diagrama que muestra cómo 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:

  1. Crea una instancia de tu clase PagingDataAdapter.
  2. Pasa la instancia PagingDataAdapter a la lista RecyclerView que deseas que muestre tus datos paginados.
  3. Observa el flujo de PagingData y pasa cada valor generado al método submitData() 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 objeto LoadState.NotLoading.
  • Si hay una operación de carga activa, LoadState es un objeto LoadState.Loading.
  • Si hay un error, LoadState es un objeto LoadState.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:

Codelabs

Ejemplos