Ao trabalhar com dados paginados, muitas vezes você precisa transformar o fluxo de dados durante o carregamento. Por exemplo, talvez seja necessário filtrar uma lista de itens ou converter itens em um tipo diferente antes de apresentá-los na interface. Outro caso de uso comum para transformação de fluxo de dados é adicionar separadores de lista.
De modo mais geral, a aplicação de transformações diretamente no fluxo de dados permite que você mantenha as construções do repositório e as construções da interface separadas.
Nesta página, você precisa estar familiarizado com o uso básico da biblioteca Paging.
Aplicar transformações básicas
Como PagingData
é
encapsulado em um fluxo reativo, é possível aplicar operações de transformação nos
dados gradualmente entre o carregamento e a apresentação dos dados.
Para aplicar transformações a cada objeto PagingData
no fluxo,
coloque as transformações dentro de uma operação
map()
no fluxo:
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. });
Converter dados
A operação mais básica em um fluxo de dados é a conversão para um tipo
diferente. Após acessar o objeto PagingData
, execute uma operação map()
em cada item individual na lista paginada dentro do objeto PagingData
.
Um caso de uso comum para isso é mapear um objeto de camada de rede ou banco de dados em um objeto especificamente usado na camada da interface. O exemplo abaixo demonstra como aplicar esse tipo de operação de mapa:
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) )
Outra conversão de dados comum é usar uma entrada do usuário, como uma string de consulta, e convertê-la na saída da solicitação para exibir. Para fazer isso, é necessário detectar e capturar a entrada de consulta do usuário, realizar a solicitação e enviar o resultado da consulta de volta à interface.
É possível detectar a entrada da consulta usando uma API de fluxo. Mantenha a referência de fluxo no
ViewModel
. A camada de interface não pode ter acesso direto a ela; em vez disso,
defina uma função para notificar o ViewModel da consulta do usuário.
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) }
Quando o valor da consulta muda no fluxo de dados, é possível executar operações para converter o valor da consulta no tipo de dados desejado e retornar o resultado para a camada da interface. A função de conversão específica depende da linguagem e do framework usado, mas todas fornecem funcionalidade semelhante.
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) );
O uso de operações como flatMapLatest
ou switchMap
garante que
apenas os resultados mais recentes retornem à interface. Se o usuário alterar a entrada da consulta
antes da conclusão da operação do banco de dados, essas operações descartarão os resultados
da consulta antiga e iniciarão a nova pesquisa imediatamente.
Filtrar dados
Outra operação comum é a filtragem. Você pode filtrar dados com base nos critérios do usuário ou remover dados da interface se eles estiverem ocultos com base em outros critérios.
É necessário colocar essas operações de filtro dentro da chamada map()
, porque
o filtro se aplica ao objeto PagingData
. Depois que os dados são filtrados do
PagingData
, a nova instância PagingData
é transmitida para a camada da interface a ser exibida.
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()) )
Adicionar separadores de lista
A biblioteca Paging é compatível com separadores de lista dinâmica. É possível melhorar a legibilidade da lista
inserindo separadores diretamente no fluxo de dados como
itens de lista RecyclerView
. Como resultado, os separadores são objetos ViewHolder
cheios de recursos,
ativando a interatividade, o foco de acessibilidade e todos
os outros recursos fornecidos por uma View
.
Há três etapas envolvidas na inserção de separadores na lista paginada:
- Converter o modelo de interface para acomodar os itens de separador.
- Transformar o fluxo de dados para adicionar dinamicamente os separadores entre o carregamento e a apresentação dos dados.
- Atualizar a interface para processar itens de separador.
Converter o modelo da interface
A biblioteca Paging insere separadores de lista no RecyclerView
como
itens de lista reais, mas os itens de separador precisam ser distinguíveis dos itens de dados
na lista para permitir que eles se vinculem a um tipo ViewHolder
diferente com uma
interface distinta. A solução é criar uma classe selada
do Kotlin
com subclasses para representar seus dados e separadores. Como alternativa, você
pode criar uma classe de base estendida pela classe de item de lista e pela
classe do separador.
Suponha que você queira adicionar separadores a uma lista paginada de itens User
. O
snippet a seguir mostra como criar uma classe de base em que as instâncias podem ser
UserModel
ou 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; } } }
Transformar o fluxo de dados
Aplique transformações ao fluxo de dados depois de carregá-lo e antes de apresentá-lo. As transformações devem fazer o seguinte:
- Converter os itens de lista carregados para refletir o novo tipo de item base.
- Usar o método
PagingData.insertSeparators()
para adicionar os separadores.
Para saber mais sobre operações de transformação, consulte Aplicar transformações básicas.
O exemplo a seguir mostra operações de transformação para atualizar o
fluxo PagingData<User>
para um fluxo PagingData<UiModel>
com separadores
adicionados:
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; } }); });
Processar separadores na interface
A etapa final é mudar a interface para acomodar o tipo de item de separador.
Crie um layout e um armazenador de visualização para os itens de separador e mude o adaptador de lista
para usar RecyclerView.ViewHolder
como o tipo de armazenador de visualização.
Assim, ele pode processar mais de um tipo de armazenador de visualização. Como alternativa, você pode definir uma classe de base
comum que é estendida pelas classes do item e do armazenador de visualização do separador.
Você também precisa fazer as seguintes mudanças no adaptador de lista:
- Adicione casos aos métodos
onCreateViewHolder()
eonBindViewHolder()
para considerar os itens de lista de separador. - Implemente um novo comparador.
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); } }
Evitar trabalhos duplicados
Um dos principais problemas a serem evitados é fazer com que o app realize um trabalho desnecessário. A busca de dados é uma operação cara, e as transformações de dados também podem ocupar um tempo valioso. Depois que os dados são carregados e preparados para exibição na interface, eles precisam ser salvos caso uma mudança de configuração ocorra e que a interface precise ser recriada.
A operação cachedIn()
armazena em cache os resultados de todas as transformações que ocorreram
antes dela. Portanto, cachedIn()
precisa ser a última chamada no 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 saber mais sobre como usar cachedIn()
com um fluxo de PagingData
, consulte
Configurar um fluxo de
PagingData.
Outros recursos
Para saber mais sobre a biblioteca Paging, consulte os seguintes recursos extras:
Codelabs
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Carregar e exibir dados paginados
- Testar a implementação da Paging
- Gerenciar e exibir estados de carregamento