Cómo transformar transmisiones de datos

Cuando trabajas con datos paginados, a menudo necesitas transformar el flujo de datos a medida que se carga. Por ejemplo, es posible que debas filtrar una lista de elementos o convertirlos en un tipo diferente antes de presentarlos en la IU. Otro caso de uso común para la transformación del flujo de datos es agregar separadores de lista.

En términos más generales, aplicar transformaciones directamente al flujo de datos te permite mantener separadas las construcciones de repositorio y las de IU.

En esta página, se asume que conoces el uso básico de la biblioteca de paginación.

Cómo aplicar transformaciones básicas

Debido a que PagingData se encapsula en un flujo reactivo, puedes aplicar operaciones de transformación a los datos de manera incremental entre la carga de los datos y su presentación.

Para aplicar transformaciones a cada objeto PagingData del flujo, ubica las transformaciones dentro de una operación map() en el flujo:

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map { pagingData ->
    // Transformations in this block are applied to the items
    // in the paged data.
}

Java

PagingRx.getFlowable(pager) // Type is Flowable<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map(pagingData -> {
    // Transformations in this block are applied to the items
    // in the paged data.
  });

Java

// Map the outer stream so that the transformations are applied to
// each new generation of PagingData.
Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData -> {
    // Transformations in this block are applied to the items
    // in the paged data.
  });

Cómo convertir datos

La operación más básica en un flujo de datos es convertirlo en un tipo diferente. Una vez que tengas acceso al objeto PagingData, podrás realizar una operación map() en cada elemento individual de la lista paginada dentro del objeto PagingData.

Un caso de uso común para esto es mapear un objeto de capa de red o de base de datos a un objeto específicamente usado en la capa de la IU. En el siguiente ejemplo, se muestra cómo aplicar este tipo de operación de mapeo:

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.map { user -> UiModel(user) }
  }

Java

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.map(UiModel.UserModel::new)
  )

Java

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.map(UiModel.UserModel::new)
)

Otra conversión de datos común es tomar una entrada del usuario, como una string de consulta, y convertirla en el resultado para mostrar de la solicitud. Para configurar esta función, se debe escuchar y capturar la entrada de consulta del usuario, realizar la solicitud y enviar el resultado de la consulta a la IU.

Puedes escuchar la entrada de la consulta con una API de transmisión. Mantén la referencia de transmisión en tu ViewModel. La capa de IU no debería tener acceso directo a ella. En su lugar, define una función para notificar a ViewModel de la consulta del usuario.

Kotlin

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = query
}

Java

private BehaviorSubject<String> querySubject = BehaviorSubject.create("");

public void onQueryChanged(String query) {
  queryFlow.onNext(query)
}

Java

private MutableLiveData<String> queryLiveData = new MutableLiveData("");

public void onQueryChanged(String query) {
  queryFlow.setValue(query)
}

Cuando el valor de la consulta cambia en el flujo de datos, puedes realizar operaciones para convertir el valor de la consulta en el tipo de datos deseado y mostrar el resultado en la capa de la IU. La función de conversión específica depende del lenguaje y el marco de trabajo utilizado, pero todos proporcionan una funcionalidad similar.

Kotlin

val querySearchResults = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  userDatabase.searchBy(query)
}

Java

Observable<User> querySearchResults =
  querySubject.switchMap(query -> userDatabase.searchBy(query));

Java

LiveData<User> querySearchResults = Transformations.switchMap(
  queryLiveData,
  query -> userDatabase.searchBy(query)
);

Usar operaciones como flatMapLatest o switchMap garantiza que solo se muestren los resultados más recientes a la IU. Si el usuario cambia su entrada de consulta antes de que se complete la operación de base de datos, estas operaciones descartan los resultados de la consulta anterior e inician la búsqueda nueva de inmediato.

Cómo filtrar datos

Otra operación común es el filtrado. Puedes filtrar datos en función de criterios del usuario o quitar datos de la IU en caso de que deban estar ocultos según otros criterios.

Debes colocar estas operaciones de filtro dentro de la llamada map() porque el filtro se aplica al objeto PagingData. Una vez que los datos se filtran fuera de PagingData, la instancia de PagingData nueva se pasa a la capa de la IU para mostrarla.

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
  }

Java

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
  )
}

Java

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
)

Cómo agregar separadores de lista

La biblioteca de Paging admite separadores de lista dinámicos. Para mejorar la legibilidad de la lista, inserta separadores directamente en el flujo de datos como elementos de lista RecyclerView. Como resultado, los separadores son objetos ViewHolder que cuentan con todas las funciones, lo cual habilita la interactividad, el enfoque de accesibilidad y todas las demás funciones que proporciona una View.

Para insertar separadores en la lista paginada, debes seguir estos tres pasos:

  1. Convierte el modelo de IU en función de los elementos del separador.
  2. Transforma el flujo de datos para agregar dinámicamente los separadores entre la carga de datos y su presentación.
  3. Actualiza la IU para manejar los elementos del separador.

Cómo convertir el modelo de IU

La biblioteca de Paging inserta separadores de lista en el RecyclerView como elementos de lista reales, pero los elementos del separador deben distinguirse de los elementos de datos de la lista para que se puedan vincular a un tipo de ViewHolder diferente en la IU. La solución es crear una clase sellada de Kotlin con subclases para representar tus datos y los separadores. Como alternativa, puedes crear una clase base que extiendan tu clase de elemento de lista y la clase del separador.

Supongamos que deseas agregar separadores a una lista paginada de elementos User. En el siguiente fragmento, se muestra cómo crear una clase base en la que las instancias pueden ser UserModel o SeparatorModel:

Kotlin

sealed class UiModel {
  class UserModel(val id: String, val label: String) : UiModel() {
    constructor(user: User) : this(user.id, user.label)
  }

  class SeparatorModel(val description: String) : UiModel()
}

Java

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

Java

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

Cómo transformar el flujo de datos

Debes aplicar transformaciones al flujo de datos después de la cargar y antes de la presentación. Las transformaciones deben realizar lo siguiente:

  • Convertir los elementos de lista cargados para reflejar el nuevo tipo de elemento base
  • Usar el método PagingData.insertSeparators() para agregar los separadores

Para obtener más información sobre las operaciones de transformación, consulta Cómo aplicar transformaciones básicas.

En el siguiente ejemplo, se muestran las operaciones de transformación para actualizar el flujo de PagingData<User> a uno de PagingData<UiModel> con separadores agregados:

Kotlin

pager.flow.map { pagingData: PagingData<User> ->
  // Map outer stream, so you can perform transformations on
  // each paging generation.
  pagingData
  .map { user ->
    // Convert items in stream to UiModel.UserModel.
    UiModel.UserModel(user)
  }
  .insertSeparators<UiModel.UserModel, UiModel> { before, after ->
    when {
      before == null -> UiModel.SeparatorModel("HEADER")
      after == null -> UiModel.SeparatorModel("FOOTER")
      shouldSeparate(before, after) -> UiModel.SeparatorModel(
        "BETWEEN ITEMS $before AND $after"
      )
      // Return null to avoid adding a separator between two items.
      else -> null
    }
  }
}

Java

// Map outer stream, so you can perform transformations on each
// paging generation.
PagingRx.getFlowable(pager).map(pagingData -> {
  // First convert items in stream to UiModel.UserModel.
  PagingData<UiModel> uiModelPagingData = pagingData.map(
    UiModel.UserModel::new);

  // Insert UiModel.SeparatorModel, which produces PagingData of
  // generic type UiModel.
  return PagingData.insertSeparators(uiModelPagingData,
    (@Nullable UiModel before, @Nullable UiModel after) -> {
      if (before == null) {
        return new UiModel.SeparatorModel("HEADER");
      } else if (after == null) {
        return new UiModel.SeparatorModel("FOOTER");
      } else if (shouldSeparate(before, after)) {
        return new UiModel.SeparatorModel("BETWEEN ITEMS "
          + before.toString() + " AND " + after.toString());
      } else {
        // Return null to avoid adding a separator between two
        // items.
        return null;
      }
    });
});

Java

// Map outer stream, so you can perform transformations on each
// paging generation.
Transformations.map(PagingLiveData.getLiveData(pager),
  pagingData -> {
    // First convert items in stream to UiModel.UserModel.
    PagingData<UiModel> uiModelPagingData = pagingData.map(
      UiModel.UserModel::new);

    // Insert UiModel.SeparatorModel, which produces PagingData of
    // generic type UiModel.
    return PagingData.insertSeparators(uiModelPagingData,
      (@Nullable UiModel before, @Nullable UiModel after) -> {
        if (before == null) {
          return new UiModel.SeparatorModel("HEADER");
        } else if (after == null) {
          return new UiModel.SeparatorModel("FOOTER");
        } else if (shouldSeparate(before, after)) {
          return new UiModel.SeparatorModel("BETWEEN ITEMS "
            + before.toString() + " AND " + after.toString());
        } else {
          // Return null to avoid adding a separator between two
          // items.
          return null;
        }
      });
  });

Cómo manejar los separadores en la IU

El último paso es cambiar la IU para adaptarla al tipo de elemento del separador. Crea un diseño y un contenedor de vistas para tus elementos del separador y cambia el adaptador de lista a fin de usar RecyclerView.ViewHolder como su tipo de contenedor de vistas de modo que pueda manejar más de un tipo de contenedor de vistas. Como alternativa, puedes definir una clase base común que extienda tanto las clases de contenedores de vistas del separador como las del elemento.

También debes realizar los siguientes cambios en tu adaptador de lista:

  • Agrega casos a los métodos onCreateViewHolder() y onBindViewHolder() para incluir los elementos de la lista del separador.
  • Implementa un comparador nuevo.

Kotlin

class UiModelAdapter :
  PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(UiModelComparator) {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ) = when (viewType) {
    R.layout.item -> UserModelViewHolder(parent)
    else -> SeparatorModelViewHolder(parent)
  }

  override fun getItemViewType(position: Int) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    return when (peek(position)) {
      is UiModel.UserModel -> R.layout.item
      is UiModel.SeparatorModel -> R.layout.separator_item
      null -> throw IllegalStateException("Unknown view")
    }
  }

  override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    position: Int
  ) {
    val item = getItem(position)
    if (holder is UserModelViewHolder) {
      holder.bind(item as UserModel)
    } else if (holder is SeparatorModelViewHolder) {
      holder.bind(item as SeparatorModel)
    }
  }
}

object UiModelComparator : DiffUtil.ItemCallback<UiModel>() {
  override fun areItemsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ): Boolean {
    val isSameRepoItem = oldItem is UiModel.UserModel
      && newItem is UiModel.UserModel
      && oldItem.id == newItem.id

    val isSameSeparatorItem = oldItem is UiModel.SeparatorModel
      && newItem is UiModel.SeparatorModel
      && oldItem.description == newItem.description

    return isSameRepoItem || isSameSeparatorItem
  }

  override fun areContentsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ) = oldItem == newItem
}

Java

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

Java

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

Cómo evitar el trabajo duplicado

Un problema clave que debes evitar es hacer que la app realice trabajos innecesarios. La recuperación de datos es una operación costosa, y las transformaciones de datos también pueden consumir tiempo valioso. Una vez que los datos se cargan y se preparan para mostrarse en la IU, deben guardarse en caso de que se produzca un cambio de configuración y se deba volver a crear la IU.

La operación cachedIn() almacena en caché los resultados de las transformaciones que ocurran. Por lo tanto, cachedIn() debería ser la última llamada de tu ViewModel.

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(viewModelScope)

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingRx.cachedIn(
  // Type is Flowable<PagingData<User>>.
  PagingRx.getFlowable(pager)
    .map(pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);
}

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingLiveData.cachedIn(
  Transformations.map(
    // Type is LiveData<PagingData<User>>.
    PagingLiveData.getLiveData(pager),
    pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);

Para obtener más información sobre el uso de cachedIn() con un flujo de PagingData, consulta Cómo configurar un flujo de PagingData.

Recursos adicionales

Para obtener más información sobre la biblioteca de Paging, consulta los siguientes recursos adicionales:

Codelabs

Ejemplos