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:
- Convierte el modelo de IU en función de los elementos del separador.
- Transforma el flujo de datos para agregar dinámicamente los separadores entre la carga de datos y su presentación.
- 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()
yonBindViewHolder()
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
- Ejemplo de Paging de los componentes de la arquitectura de Android
- Ejemplo de Paging de los componentes de la arquitectura de Android con base de datos y red
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo cargar y mostrar datos paginados
- Cómo probar tu implementación de Paging
- Cómo administrar y presentar estados de carga