Construindo um adaptador reativo e heterogêneo em Kotlin

Tempo de leitura: 10 minutes

Se você estiver no desenvolvimento do Android por um tempo, pode ter enfrentado uma situação em que precisa implementar vários tipos de visualização na reciclagem. A maneira muito comum pela qual você pode ter encontrado essa situação é durante a implementação de paginação.

 

Maneira convencional

Ao implementar a paginação, você precisa implementar o carregamento no final da lista ao executar uma chamada de serviço e a maneira muito comum de fazer isso é usando tipos de visualização.

class ItemViewHolder extends RecyclerView.ViewHolder{
    
}

class LoadingViewHolder extends RecyclerView.ViewHolder{
   
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return if (viewType == LOADING) {
        LoadingViewHolder(mLayoutInflater.inflate(R.layout.loading, parent, false))
    } else {
        ItemViewHolder(mLayoutInflater.inflate(R.layout.item_layout, parent, false))
    }
}

OK, então está tudo bem porque você só precisa implementar o carregamento, mas e se você precisar implementar layouts diferentes, conforme mostrado na imagem

Se você seguir o padrão acima para aumentar os layouts para diferentes tipos de visualização, terá um grande problema.

Usar blocos if/else ou switch nos viewtypes e onCreateViweHolder só resolve o problema para alguns extintos.

Para resolver o problema completamente, você precisa implementar uma solução genérica que funcione para qualquer tipo de visualização sem qualquer avelã de blocos if/else.

 

 

 

 

 

 

Sempre que você se pegar escrevendo um código na forma “se o objeto for do tipo T1, faça algo, mas se for do tipo T2, faça outra coisa”, dê um tapa em si mesmo.
– Danny Preussler

 

Uma maneira melhor e mais eficiente

Aperte o cinto e vai ser longo e contém material técnico aprofundado, pegue uma xícara de café e fique à vontade. Se você não entendeu alguma parte do artigo, entre em contato comigo pelo twitter ou deixe suas dúvidas nos comentários. Para deixá-lo ainda mais confortável, implementei um projeto de amostra do GitHub e deixei o link do repo no final do artigo.

Primeiro, você precisa atualizar para ListAdapter de RecyclerviewAdapter. Isso não tem nada a ver com o processo que estamos fazendo aqui, mas usando ListAdapter podemos implementar DiffUtil facilmente e é obrigatório para ListAdapter para que não o ignoremos.

Para quem não sabe o que é DiffUtil

DiffUtil é uma classe de utilitário que pode calcular a diferença entre duas listas e gerar uma lista de operações de atualização que converte a primeira lista na segunda.

Então, usando este DiffUtil quando você atualiza o recyclerview com uma nova lista. O DiffUtil verifica quais itens foram alterados da lista existente para a lista que você aprovou recentemente e cria uma nova lista com esses itens.

Em seguida, atualize o adaptador com a nova lista para que somente então os itens alterados sejam atualizados. É legal né !!!!

Para conhecer melhor o DiiUtil, clique aqui

O código e os padrões que você está vendo aqui são do aplicativo Android Dev 2019. Dê uma olhada no código-fonte

Passo 1

Primeiro, crie uma classe abstrata que estende DiffUtil.ItemCallback que encapsula a lógica para criar e vincular um ViewHolder para um tipo de item. Dar uma olhada

abstract class FeedItemViewBinder<M, in VH : ViewHolder>(
    val modelClass: Class<out M>) : DiffUtil.ItemCallback<M>() {

    abstract fun createViewHolder(parent: ViewGroup): ViewHolder
    abstract fun bindViewHolder(model: M, viewHolder: VH)
    abstract fun getFeedItemType(): Int

    // Tê-los como não abstratos porque nem todos os viewBinders são necessários para implementá-los.
    open fun onViewRecycled(viewHolder: VH) = Unit
    open fun onViewDetachedFromWindow(viewHolder: VH) = Unit
}

onViewRecycled e onViewDetachedFromWindow estão abertos porque nem todo visualizador precisa implementá-los.

Você entenderá como usamos essa classe de fichário para vincular um portador de visualização com o adaptador em algum tempo. Por enquanto, pense nele como um suporte de visualização de base.

 

Passo 2

Implemente uma classe DiffUtil genérica que funcione para qualquer classe de modelo, conforme mostrado abaixo

typealias FeedItemClass = Class<out Any>
typealias FeedItemBinder = FeedItemViewBinder<Any, ViewHolder>

internal class FeedDiffCallback(
    private val viewBinders: Map<FeedItemClass, FeedItemBinder>
) : DiffUtil.ItemCallback<Any>() {

    override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
        if (oldItem::class != newItem::class) {
            return false
        }
        return viewBinders[oldItem::class.java]?.areItemsTheSame(oldItem, newItem) ?: false
    }

    override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
        return viewBinders[oldItem::class.java]?.areContentsTheSame(oldItem, newItem) ?: false
    }
}

Aqui, você deve criar dois objetos typealias, um para classes de modelo e outro para vincular a classe de modelo com visualizador específico como pares chave-valor.

Por padrão, a classe DiffUtil analisa o conteúdo dos itens são alterados ou não com duas funções areItemsTheSame e areContentsTheSame

No FeedDiffCallback você deve fazer uma codificação genérica para que, independentemente da classe do modelo, ela funcione

Portanto, na função areItemsThemsame, primeiro você deve verificar se os tipos de itens antigos e novos são iguais ou não e, em seguida, usamos os viewbinders que são passados ​​aqui através do construtor para verificar se o conteúdo foi alterado ou não.

O mesmo ocorre com a função areContentsTheSame.

 

Passo 3

Neste ponto, fizemos todo o trabalho necessário para criar um adaptador heterogêneo. Precisamos apenas criar um adaptador e usar o viewbinder e os DiffUtils personalizados que criamos acima.

Dê uma olhada no código do adaptador

class FeedAdapter(
    private val viewBinders: Map<FeedItemClass, FeedItemBinder>
) : ListAdapter<Any, ViewHolder>(FeedDiffCallback(viewBinders)) {

    private val viewTypeToBinders = viewBinders.mapKeys { it.value.getFeedItemType() }

    private fun getViewBinder(viewType: Int): FeedItemBinder = viewTypeToBinders.getValue(viewType)

    override fun getItemViewType(position: Int): Int =
        viewBinders.getValue(super.getItem(position).javaClass).getFeedItemType()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return getViewBinder(viewType).createViewHolder(parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        return getViewBinder(getItemViewType(position)).bindViewHolder(getItem(position), holder)
    }

    override fun onViewRecycled(holder: ViewHolder) {
        getViewBinder(holder.itemViewType).onViewRecycled(holder)
        super.onViewRecycled(holder)
    }

    override fun onViewDetachedFromWindow(holder: ViewHolder) {
        getViewBinder(holder.itemViewType).onViewDetachedFromWindow(holder)
        super.onViewDetachedFromWindow(holder)
    }
}

Primeiro, você deve passar um mapa com todas as classes de modelo e ViewHolder no construtor, conforme mostrado acima. Você pode ver como fazer isso na próxima etapa.

Em seguida, criamos um mapa com o nome viewTypeToBinders usando a função mapkeys nos viewbinders que são passados ​​no construtor

Qual é o propósito da função mapkeys?
Se quaisquer duas entradas forem mapeadas para as chaves iguais, o valor da última substituirá o valor associado à anterior. O mapa retornado preserva a ordem de iteração de entrada do mapa original.

Para que o mapa viewTypeToBinders não tenha entradas duplicadas de visualizadores.

Agora, é hora de lidar com as funções que são substituídas como getItemViewType, onCreateViewHolder, etc. Em geral, essas funções retornam os valores ou acionam as funções com base em viewtypes.

Esta é a parte mais complicada, você tem que lidar com ela com cuidado porque os valores que você retorna aqui devem ser genéricos. Vamos ver como fazer

Basicamente, quando você cria o visualizador, você precisa estendê-lo com FeedItemViewBinder, pois é uma classe abstrata, as funções da classe devem ser implementadas na classe estendida e os valores que fornecemos no visualizador específico serão refletidos no adaptador como

getFeedItemType retorna Int, portanto, no viewHolder real, passamos o layout diretamente e usamos esse layout como viewtype no adaptador, que é genérico para todos os viewtypes.

Vou explicar como este adaptador funciona com as duas funções

getItemViewType

Retorne o tipo de visualização do item na posição para fins de reciclagem de visualização.

Aqui, usamos viewbinder’s que são passados ​​no construtor para recuperar o viewtype usando um item presente que obtivemos através da posição.

override fun getItemViewType(position: Int): Int =

viewBinders.getValue(super.getItem(position).javaClass).getFeedItemType()

onCreateViewHolder

Chamado quando RecyclerView precisa de um novo ViewHolder do tipo fornecido para representar um item.

Então, basicamente, essa função cria um viewholder com base no tipo de visualização. Lembre-se da função getViewBinder que criamos no início do adaptador e a usamos agora para obter o viewholder apropriado com base no tipo de visualização que obtivemos nos parâmetros de onCreateViewHolder.

Em seguida, chame createViewHolder nessa classe de titular de visualização específica.

Então, o que basicamente acontece aqui é que todo o código que fazemos em um adaptador, como a distinção de viewtypes, viewholders, etc., é movido para viewholders específicos por meio de uma classe abstrata (FeedItemViewBinder) que usa uma classe de modelo e tipos de viewholder como parâmetros. Portanto, em vez de escrever todo o código, extraímos os dados do próprio viewholder.

 

Passo 4

É hora de criar visualizadores que implementamos no adaptador. A criação de um visualizador para esse adaptador heterogêneo envolve duas etapas. Primeiro, precisamos criar um fichário de visualização que vincule o visualizador atual ao adaptador e, em seguida, a classe do visualizador real. Dar uma olhada

class VerticalImagesViewBinder(val block : (data: VeriticalImageModel) -> Unit) : FeedItemViewBinder<VeriticalImageModel, VerticalImagesViewHolder>(
    VeriticalImageModel::class.java) {

    override fun createViewHolder(parent: ViewGroup): VerticalImagesViewHolder {
        return VerticalImagesViewHolder(
            LayoutInflater.from(parent.context).inflate(getFeedItemType(), parent, false),block)
    }

    override fun bindViewHolder(model: VeriticalImageModel, viewHolder: VerticalImagesViewHolder) {
        viewHolder.bind(model)
    }

    override fun getFeedItemType() = R.layout.adapter_vertical_image

    override fun areContentsTheSame(oldItem: VeriticalImageModel, newItem: VeriticalImageModel) = oldItem == newItem

    override fun areItemsTheSame(oldItem: VeriticalImageModel, newItem: VeriticalImageModel) : Boolean {
        return oldItem.Image == newItem.Image
    }
}


class VerticalImagesViewHolder(val view : View, val block : (data: VeriticalImageModel) -> Unit)
    : RecyclerView.ViewHolder(view) {

    fun bind(data: VeriticalImageModel) {

        itemView.setOnClickListener {
            block(data)
        }

        itemView.apply {
            Glide
                .with(itemView.context)
                .load(data.Image)
                .centerCrop()
                .into(im_vertical)
        }
    }
}

Então, vendo o código acima, muitos de vocês perceberam como esse código funciona.

Para quem não entende

Por meio do viewbinder que estendemos com FeedItemViewBinder, implemente as funções que são chamadas no adaptador e retorne layouts apropriados e funções de atualização.

Você pode criar quantos suportes de visualização quiser, isso é tudo que você não precisa fazer nada no adaptador. O adaptador é projetado para ser genérico com os visualizadores

Você pode criar quantos suportes de visualização quiser, isso é tudo que você não precisa fazer nada no adaptador. O adaptador é projetado para ser genérico com os visualizadores

val block : (data: VeriticalImageModel) -> Unit

 

Passo 5

Agora, é hora de criar o adaptador e atribuí-lo à visualização de reciclagem.

Para criar o adaptador, precisamos passar um mapa com classe de modelo e viewbinders como pares chave-valor. dar uma olhada

private var adapter: FeedAdapter? = null

fun verticalImageClick(data : VeriticalImageModel){
        ...
}

private fun showFeedItems(recyclerView: RecyclerView, list: ArrayList<Any>?) {
    if (adapter == null) {
        val viewBinders = mutableMapOf<FeedItemClass, FeedItemBinder>()
        val verticalImagesViewBinder = VerticalImagesViewBinder { data : VeriticalImageModel ->
            verticalImageClick(data)}
        @Suppress("UNCHECKED_CAST")
        viewBinders.put(
            verticalImagesViewBinder.modelClass,
            verticalImagesViewBinder as FeedItemBinder)
        adapter = FeedAdapter(viewBinders)
    }
    recyclerView.apply {
        layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL,false)
    }
    if (recyclerView.adapter == null) {
        recyclerView.adapter = adapter
    }
    (recyclerView.adapter as FeedAdapter).submitList(list ?: emptyList())
}

Primeiro, você deve criar todas as instâncias de view binder de que precisa e colocá-las no mapa, conforme mostrado acima. A principal coisa que você precisa lembrar aqui é que você precisa criar todos os tipos de visualização aqui no início, mesmo os itens de tipo de visualização específicos não estão presentes na lista, mas eles podem vir na paginação. Então, a única coisa que resta é atribuir o adaptador para visualização de reciclagem.

Neste ponto, você pode implementar qualquer número de viewtypes que quiser sem adicionar um pouco de código ao seu adaptador e essa é a beleza. Então, isso é bom e vamos para o próximo estágio e se você precisar ter visualizações de reciclagem aninhadas. Este adaptador irá suportá-los?

 

Nested Recyclerviews

O FeedAdapter que criamos aqui é o mais poderoso e genérico que pode funcionar com o recyclerview aninhado sem nenhuma alteração no adaptador. Podemos criar o viewholder com recyclerview aninhado como criamos um viewholder normal. Dar uma olhada

class HorizontalImagesListViewBinder(val block : (data: HorizontalImageModel) -> Unit)
    : FeedItemViewBinder<HorizontalImageListModel, HorizontalImagesListViewHolder>(
    HorizontalImageListModel::class.java) {

    override fun createViewHolder(parent: ViewGroup): HorizontalImagesListViewHolder {
        return HorizontalImagesListViewHolder(
            LayoutInflater.from(parent.context).inflate(getFeedItemType(), parent, false),block)
    }

    override fun bindViewHolder(model: HorizontalImageListModel, viewHolder: HorizontalImagesListViewHolder) {
        viewHolder.bind(model)
    }

    override fun getFeedItemType() = R.layout.adapter_recycleriew

    override fun areContentsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) = oldItem == newItem

    override fun areItemsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) : Boolean {
        return oldItem.id == newItem.id
    }
}


class HorizontalImagesListViewHolder(val view : View, val block : (data: HorizontalImageModel) -> Unit)
    : RecyclerView.ViewHolder(view) {

    fun bind(data: HorizontalImageListModel) {

        var adapter : FeedAdapter? = null

        itemView.apply {
            val horizontalImagesViewBinder = HorizontalImagesViewBinder { horizontalImageModel : HorizontalImageModel ->
                block(horizontalImageModel)}
            val viewBinders = mutableMapOf<FeedItemClass, FeedItemBinder>()
            @Suppress("UNCHECKED_CAST")
            viewBinders.put(
                horizontalImagesViewBinder.modelClass,
                horizontalImagesViewBinder as FeedItemBinder)
            adapter = FeedAdapter(viewBinders)
            tv_horizontal_header?.text = data.title
            adapter_recycllerview?.apply {

                layoutManager = LinearLayoutManager(adapter_recycllerview?.context,
                    LinearLayoutManager.HORIZONTAL,false)
                if (adapter_recycllerview?.adapter == null) {
                    adapter_recycllerview?.adapter = adapter
                }
                (adapter_recycllerview?.adapter as FeedAdapter).submitList(
                    data.Images as List<Any>? ?: emptyList())
            }
        }
    }
}

Como você pode ver, o código aqui não é muito diferente do código do visualizador normal. A única diferença é que temos que criar a instância do adaptador interno e criar o mapa de viewholders e classes de modelo e atribuí-los a recyclerview. Isso resulta na saída abaixo

Se você observar cuidadosamente quando rolarmos a visualização de reciclagem horizontal aninhada e a rolagem sobre a visualização de reciclagem pai até o final e novamente rolarmos de volta para a visualização de reciclagem horizontal, a posição de rolagem da visualização de reciclagem aninhada está descansando no primeiro item. Podemos fazer melhor, sim, podemos e vamos ver como podemos resolver esse problema

O que basicamente temos que fazer é salvar o getLayoutManagerState do recyclerview aninhado e restaurar a instância quando esse item for novamente anexado à view.

a única diferença é que você deve implementar as funções onViewRecycled e onViewDetachedFromWindow de FeedItemViewBinder.

O que acontece internamente aqui é que quando você configura o adaptador para recyclerview aninhado, vamos salvar o getLayoutManagerState dessa recyclerview e quando você chegar a essa recyclerview aninhada, restauraremos o estado antigo para o gerenciador de layout Dar uma olhada

private lateinit var horizontalImagesViewBinder : HorizontalImagesListViewBinder

companion object {
        private const val BUNDLE_KEY_LAYOUT_HORIZONTAL_LIST_STATE = "horizontal_list_layout_manager"
}

override fun onSaveInstanceState(outState: Bundle) {
        if (::horizontalImagesViewBinder.isInitialized) {
            outState.putParcelable(
                BUNDLE_KEY_LAYOUT_HORIZONTAL_LIST_STATE,
                horizontalImagesViewBinder.recyclerViewManagerState
            )
        }
        super.onSaveInstanceState(outState)
}

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

        horizontalImagesViewBinder = HorizontalImagesListViewBinder ({ data : HorizontalImageModel ->
            horizontalImageClick(data)}, savedInstanceState?.getParcelable(
            BUNDLE_KEY_LAYOUT_HORIZONTAL_LIST_STATE
        ))

        addData()
}

class HorizontalImagesListViewBinder(val block : (data: HorizontalImageModel) -> Unit,
                                     var recyclerViewManagerState: Parcelable? = null)
    : FeedItemViewBinder<HorizontalImageListModel, HorizontalImagesListViewHolder>(
    HorizontalImageListModel::class.java) {

    override fun createViewHolder(parent: ViewGroup): HorizontalImagesListViewHolder {
        return HorizontalImagesListViewHolder(
            LayoutInflater.from(parent.context).inflate(getFeedItemType(), parent, false),block)
    }

    override fun bindViewHolder(model: HorizontalImageListModel, viewHolder: HorizontalImagesListViewHolder) {
        viewHolder.bind(model,recyclerViewManagerState)
    }

    override fun getFeedItemType() = R.layout.adapter_recycleriew

    override fun areContentsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) = oldItem == newItem

    override fun areItemsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) : Boolean {
        return oldItem.id == newItem.id
    }

    override fun onViewRecycled(viewHolder: HorizontalImagesListViewHolder) {
        saveInstanceState(viewHolder)
    }

    override fun onViewDetachedFromWindow(viewHolder: HorizontalImagesListViewHolder) {
        saveInstanceState(viewHolder)
    }

   // Saving the present instance of the recyclerview every time before the nested recyclerview is detached or the view get recycled
    fun saveInstanceState(viewHolder: HorizontalImagesListViewHolder) {
        if (viewHolder.adapterPosition == RecyclerView.NO_POSITION) {
            return
        }
        recyclerViewManagerState = viewHolder.getLayoutManagerState()
    }

}


class HorizontalImagesListViewHolder(val view : View, val block : (data: HorizontalImageModel) -> Unit)
    : RecyclerView.ViewHolder(view) {

    private var layoutManager: RecyclerView.LayoutManager? = null

    fun bind(data: HorizontalImageListModel,layoutManagerState: Parcelable?) {

        var adapter : FeedAdapter? = null

        itemView.setOnClickListener {

        }

        itemView.apply {
            val horizontalImagesViewBinder = HorizontalImagesViewBinder { horizontalImageModel : HorizontalImageModel ->
                block(horizontalImageModel)}
            val viewBinders = mutableMapOf<FeedItemClass, FeedItemBinder>()
            @Suppress("UNCHECKED_CAST")
            viewBinders.put(
                horizontalImagesViewBinder.modelClass,
                horizontalImagesViewBinder as FeedItemBinder)
            adapter = FeedAdapter(viewBinders)
            tv_horizontal_header?.text = data.title
            adapter_recycllerview?.apply {

                layoutManager = LinearLayoutManager(adapter_recycllerview?.context,
                    LinearLayoutManager.HORIZONTAL,false)

            }
            layoutManager = adapter_recycllerview?.layoutManager
            if (adapter_recycllerview?.adapter == null) {
                adapter_recycllerview?.adapter = adapter
            }
            (adapter_recycllerview?.adapter as FeedAdapter).submitList(
                data.Images as List<Any>? ?: emptyList())
            if (layoutManagerState != null) {
                // restoring the old instance so that scroll position is synced
                layoutManager?.onRestoreInstanceState(layoutManagerState)
            }
        }
    }

    // Saving the present instance of the recyclerview
    fun getLayoutManagerState(): Parcelable? = layoutManager?.onSaveInstanceState()

}

Aqui, sempre que a visualização de reciclagem aninhada foi separada da janela ou reciclada, estamos salvando o estado atual do gerenciador de layout que está anexado à visualização de reciclagem interna e, posteriormente, sempre que a visualização for anexada novamente, estamos restaurando a posição de rolagem.

Isso é tudo, o adaptador de feed que criamos funciona com qualquer número de viewtypes e, ao mesmo tempo, qualquer número de recyclerviews aninhados sem alterar nada no adaptador.

A beleza desse código do Google é que nada é tratado diretamente no adaptador, mas tudo é operado no adaptador. Isso soa meio insano, mas é verdade e você vai dizer isso também se entender o padrão aqui.

 

Links Úteis

SG-K/HetrogeniousAdapter

google/iosched