Transformar fluxos de dados

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:

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.
}

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:

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

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.

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = 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.

val querySearchResults: Flow<User> = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  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.

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

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 combináveis no seu layout. Como resultado, os separadores são elementos combináveis cheios de recursos, permitindo total interatividade, estilização e semântica de acessibilidade.

Há três etapas envolvidas na inserção de separadores na lista paginada:

  1. Converter o modelo de interface para acomodar os itens de separador. Uma maneira de fazer isso é encapsular o item de dados e o separador em uma única classe sealed. Isso permite que a interface processe vários tipos de itens na mesma lista.
  2. Transformar o fluxo de dados para adicionar dinamicamente os separadores entre o carregamento e a apresentação dos dados.
  3. Atualizar a interface para processar itens de separador.

Converter o modelo da interface

A biblioteca Paging insere separadores de lista na interface como itens de lista reais, mas os itens de separador precisam ser distinguíveis dos itens de dados na lista para garantir que os dois tipos combináveis sejam renderizados de maneira 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:

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()
}

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:

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
    }
  }
}

Processar separadores na interface

A etapa final é mudar a interface para acomodar o tipo de item de separador. Em um layout de carregamento lento, é possível processar vários tipos de itens verificando o tipo de cada UiModel emitido. Ao iterar pelos dados paginados, use uma instrução when para chamar o elemento combinável adequado. Isso permite fornecer uma interface distinta para itens de dados e separadores.

@Composable fun UserList(pagingItems: LazyPagingItems) {
  LazyColumn {
    items(
      count = pagingItems.itemCount,
      key = { index ->
        val item = pagingItems.peek(index)
        when (item) {
          is UiModel.UserModel -> item.user.id
          is UiModel.SeparatorModel -> item.description
          else -> index
        }
      }
    ) { index ->
      when (val item = pagingItems[index]) {
        is UiModel.UserModel -> UserItemComposable(item.user)
        is UiModel.SeparatorModel -> SeparatorComposable(item.description)
        null -> PlaceholderComposable()
      }
    }
  }
}

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. Normalmente, você aplica esse operador no ViewModel antes de expor o Flow aos elementos combináveis.

Para gerenciar o cache corretamente, transmita um CoroutineScope para cachedIn(), conforme mostrado no exemplo a seguir usando viewModelScope.

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(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:

Documentação

Visualiza conteúdo