Explorando Paging3

Tempo de leitura: 6 minutes

Neste artigo, você aprenderá como e por que precisamos usar Paging3 – a biblioteca jetpack. Você também aprenderá como ele é diferente de suas versões anteriores. Você também encontrará um guia detalhado sobre como implementar paging3 na arquitetura MVVM usando a API Spotify.

Um repositório GitHub com uma amostra de trabalho incluindo todas as coisas que discuti aqui está vinculado na parte inferior deste artigo. Sinta-se à vontade para brincar com ele.

Introdução

Paging é uma biblioteca Jetpack projetada para ajudar os desenvolvedores a carregar e exibir páginas de dados de grandes conjuntos de dados com uso mínimo de recursos de rede e sistema.

A biblioteca de paginação pode carregar os dados de uma fonte remota ou armazenamento local. O modo offline é essencial para uma experiência eficaz do usuário; é recomendado obter dados de uma fonte remota e salvá-los localmente. Em seguida, use o armazenamento local como uma única fonte de verdade. A biblioteca Paging3 cobre tudo isso fora da caixa.

Além disso, os componentes do paging3 são desenvolvidos para se adequar à arquitetura de aplicativo Android recomendada e também para fornecer melhor suporte a Kotlin.

Como é diferente das versões anteriores?

Um dos principais aprimoramentos de suas versões anteriores de paginação é que agora é a primeira biblioteca Kotlin com o poder da API de fluxo do repositório aos componentes da IU.

Uma das melhorias significativas é que podemos adicionar um rodapé personalizado, cabeçalho, carregamento e itens de nova tentativa à lista de paginação com menos código clichê. Também podemos usar filtro, mapa e outros operadores para aplicar as transformações necessárias antes de exibir os dados.

Para integrar a biblioteca de paginação perfeitamente com arquiteturas recomendadas, temos vários componentes de paginação, como PagingConfig, PagingSource, LoadResult, PagingDataAdapter e mais. Você aprenderá sobre eles nas seções a seguir deste artigo.

Integração

No momento em que eu estava escrevendo este artigo, a biblioteca paging3 ainda estava no estágio alfa. Então, eu recomendo que você verifique a versão mais recente se você for do futuro 😉.

Integrar o paging V3 é tão simples quanto integrar qualquer outra biblioteca Jetpack. Você precisa adicionar a seguinte linha na tag dependencies no arquivo build.gradle de nível de aplicativo.

implementation "androidx.paging:paging-runtime:3.0.0-alpha03"

PagingSource

Carregar grandes conjuntos de dados de uma fonte remota ou armazenamento local é uma operação cara. PagingSource é um componente da biblioteca de paginação para lidar com isso da melhor maneira possível.

A importância do PagingSource reside nos genéricos de seus parâmetros. Possui dois parâmetros: Chave e valor. O tipo de dados de ambos os parâmetros é Any. O valor representa a classe de dados do item da lista de paginação.

A chave é outro parâmetro para definir quais dados carregar. Como seu tipo de dados é Any, podemos usá-lo para representar um número de página ou o número de itens por página ou um tipo personalizado que seu servidor suporta. Esse tipo de carregamento de dados abre as portas da biblioteca de paginação para uma ampla gama de desenvolvedores.

Chega de conversa, vamos codificar. Para construir um PagingSource, precisamos de uma classe de modelo e uma solicitação de API. Aqui eu usei a API Spotify com Retrofit. Não vou discutir a configuração de Retrofit e API, mas você pode encontrá-lo no repositório GitHub vinculado na parte inferior.

Supondo que você tenha configurado o cliente de rede e os terminais de API, a próxima etapa é projetar o índice de paginação do Spotify. A API do Spotify não segue resultados incrementais com base no número da página, mas usa deslocamento e limite para recuperar os dados do servidor.

Como funciona o Spotify Paging?

Veja o seguinte URL como exemplo:

https://api.spotify.com/v1/browse/categories?country=IN&limit=5&offset=10

Neste exemplo, em uma lista de 42 categorias (total) da API do Spotify: Da décima categoria (offset), recupere as próximas cinco categorias (limit).

Projete uma lógica de paginação do Spotify

É fácil; precisamos aumentar o deslocamento para a próxima solicitação de paging com o tamanho limite após completar a presente solicitação. Como não se baseia apenas no número da página, criei uma classe de dados personalizada para manter os índices necessários no PagingSource. Dê uma olhada:

data class SptifyPagingIndex(val limit : Int, val offest : Int, val total : Int)

fun paggingIndexing(offest : Int, totalFromServer : Int,
                    totalFromPagging : Int  ): Pair<SptifyPagingIndex?,SptifyPagingIndex?> 
    var nextKeyOffest : Int? = offest + loadSize
    if (offest != 0 && offest >= totalFromPagging){
        nextKeyOffest = null
    }
    var previousKeyOffest : Int? = offest
    if (offest == 0){
        previousKeyOffest = null
    }
    val previousKey = previousKeyOffest?.let {
        SptifyPagingIndex(limit= loadSize,offest = it ,total = totalFromServer)
    }
    val nextKey = nextKeyOffest?.let {
        SptifyPagingIndex(limit= loadSize,offest = it,total = totalFromServer)
    }
    return Pair(previousKey, nextKey)
}

Finalmente, chegamos; é hora de implementar o PagingSource, a parte central da biblioteca de paginação. Dar uma olhada:

private const val loadSize = 5
private val INTIAL_SPOTIFY_PAGE_INDEX = SptifyPagingIndex(limit = loadSize,offest = 0,total = 0)

class SpotifyPagingSource(private val spotifyAPI: SpotifyAPI)
      : PagingSource<SptifyPagingIndex, CategoryItem>() {

    override suspend fun load(params: LoadParams<SptifyPagingIndex>): LoadResult<SptifyPagingIndex, CategoryItem> {
        val limit = params.key?.limit ?: INTIAL_SPOTIFY_PAGE_INDEX.limit
        val offest = params.key?.offest ?: INTIAL_SPOTIFY_PAGE_INDEX.offest
          
        return try {
              
            val response = spotifyAPI.getCategories("IN","$limit","$offest")
            val categories = response.categories?.items?:ArrayList()
            val (previousKey, nextKey) = paggingIndexing(offest = offest,
                totalFromServer = response.categories?.total?:0,
                totalFromPagging = params.key?.total?:0)

            LoadResult.Page(
                data = categories,
                prevKey = previousKey,
                nextKey =nextKey)
              
        } catch (exception: IOException) {
            return LoadResult.Error(exception)
        } catch (exception: HttpException) {
            return LoadResult.Error(exception)
        }
    }
}

Se estivermos solicitando pela primeira vez, escolhemos os valores de limite e deslocamento de INTIAL_SPOTIFY_PAGE_INDEX, caso contrário, os escolhemos nos LoadParams fornecidos por SpotifyPagingSource.

spotifyAPI é uma interface de API de atualização que usamos para invocar a solicitação de categorias. Assim que recebermos a resposta, temos que atualizar as chaves anterior e seguinte, que SpotifyPagingSource irá fornecer como LoadParams para a próxima solicitação. Então, podemos postar a lista e as chaves para a classe lacrada LoadResult para usá-las no outro lado (ViewModel).

Repositório

Agora que concluímos o pagingsource, a próxima etapa é invocá-lo de nosso repositório. Fazemos isso usando o Pager, um ponto de entrada principal no Paging, o construtor de um fluxo reativo de PagingData.

Ele tem dois parâmetros obrigatórios: config e pagingSourceFactory.

PagingConfig

Um objeto usado para configurar o comportamento de carregamento em um Pager, à medida que carrega conteúdo de um PagingSource. Podemos configurar vários atributos, como

  • Contagem de carga inicial
  • Se deve habilitar marcadores de posição durante o carregamento
  • A distância de pré-busca, que define a distância da borda do acesso ao conteúdo carregado, deve estar para acionar o carregamento posterior.
  • PaseSize é usado para definir o número de itens a serem buscados no PagingSource de uma vez.
class SpotifyRepo(private val spotifyAPI: SpotifyAPI) {

    fun getPaggingCategories() : Flow<PagingData<CategoryItem>> {
        return Pager(
            config = PagingConfig(pageSize = loadSize,
                enablePlaceholders = false),
            pagingSourceFactory = { SpotifyPagingSource(spotifyAPI) }
        ).flow
    }

}

Demos tamanho de página de cinco e marcadores de posição desativados por enquanto, por meio do parâmetro pagingSourceFactory, precisamos passar SpotifyPagingSource. Finalmente, precisamos converter no fluxo para observá-lo a partir do modelo de visualização ou dos componentes da IU.

ViewModel

Agora que configuramos o Pager no repositório, a próxima etapa é invocá-lo a partir do modelo de visualização. Para garantir o uso de fluxo restrito a ViewModel, usamos a função em cache no fluxo com o escopo do modelo de visualização. Dar uma olhada:

class CategoriesViewModel(val spotifyRepo: SpotifyRepo) : ViewModel() {

    val job = Job()
    val scope = CoroutineScope(job + Dispatchers.IO)
    private var currentSearchResult: Flow<PagingData<CategoryItem>>? = null

    fun searchRepo(): Flow<PagingData<CategoryItem>> {
        val lastResult = currentSearchResult
        if (lastResult != null) {
            return lastResult
        }
        val newResult: Flow<PagingData<CategoryItem>> = spotifyRepo.getPaggingCategories()
            .cachedIn(scope)
        currentSearchResult = newResult
        return newResult
    }

    override fun onCleared() {
        super.onCleared()
        scope.coroutineContext.cancelChildren()
    }

}

spotify categorias view-model

PagingDataAdapter

É hora de implementar o adaptador, aqui usamos PagingDataAdapter da biblioteca paging3 que estende RecyclerView.Adapter. É muito semelhante a implementar o adaptador Recyclerview. Dar uma olhada:

class CategoriesAdapter(private val glideDeligate: JlGlideDeligate) :
    PagingDataAdapter<CategoryItem, CategoriesAdapter.CategoriesViewHolder>(diffCallback){


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoriesViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val v = layoutInflater.inflate(R.layout.lay_list_item, parent, false)
        return CategoriesViewHolder(v,glideDeligate)
    }

    override fun onBindViewHolder(holder: CategoriesViewHolder, position: Int) {
        getItem(position)?.let {
            holder.onBindView(it,position)
        }
    }


    inner class CategoriesViewHolder(
        itemView: View, val glideDeligatee: JlGlideDeligate
    ) : RecyclerView.ViewHolder(itemView) {

        fun onBindView(data: CategoryItem, position: Int) {
            itemView.apply {
                data.icons?.let {
                    if (it.isNotEmpty() &&
                        !it[0].url.isNullOrEmpty())
                        glideDeligatee.load(im_categories,it[0].url!!)
                }

                tv_category?.text = data.name
            }
        }
    }

    companion object {
        
        private val diffCallback = object : DiffUtil.ItemCallback<CategoryItem>() {
            override fun areItemsTheSame(oldItem: CategoryItem, newItem: CategoryItem): Boolean =
                oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: CategoryItem, newItem: CategoryItem): Boolean =
                oldItem == newItem
        }
    }

}

Não vou discutir o adaptador, pois é uma implementação fundamental de um adaptador Recyclerview.

Acionar atualizações de rede

Agora que concluímos a configuração do adaptador e da fonte de paginação, é hora de acionar as atualizações de rede. Precisamos invocar collectLatest no fluxo do modelo de visualização, que retorna um receptor. Dentro do receptor, podemos atualizar o adaptador com o conjunto de dados mais recente. Dar uma olhada:

class CategoriesActivity : AppCompatActivity() {

    private val categoriesViewModel : CategoriesViewModel by viewModel()
    lateinit var adapter: CategoriesAdapter
    lateinit var glideDeligate: JlGlideDeligate

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        glideDeligate = JlGlideDeligate(this)
        adapter = CategoriesAdapter(glideDeligate)
        categories_recyclerView?.adapter = adapter

        categoriesViewModel.scope.launch {
            categoriesViewModel.searchRepo().collectLatest {
                adapter.submitData(it)
            }

        }
    }

}

Todo o resto será feito pela biblioteca de paginação. Você pode encontrar um exemplo de trabalho no repositório GitHub aqui. Na próxima parte deste artigo, você aprenderá como usar o mapa, filtro e outras funções para implementar transformações e adicionar carregamento, rodapé, cabeçalho e layouts personalizados de repetição ao adaptador de paginação.

“Nota: Por favor, crie sua própria chave de API do Spotify a partir daqui. Em seguida, substitua-a pela chave existente no projeto.”

Isso é tudo por agora, espero que você tenha aprendido algo útil, obrigado pela leitura.