Uma versão simples de uma visão heterogênea do reciclador no modelo Visitante

Seis meses se passaram desde que saí de Pascalem kotlin e me apaixonei pelo desenvolvimento do Android, e agora já me permito escalar publicamente com minhas ideias o mosteiro de outra pessoa. Mas há uma razão para isso. Tendo assistido em chats de perfil quais questões surgem com mais frequência para desenvolvedores Android, e não apenas para iniciantes, percebi que na maioria dos casos, quando uma pessoa encontra um erro que não consegue entender, pois não consegue entender a explicação dos colegas do chat ou suas principais questões, o motivo é o uso impensado de partes de código ou bibliotecas prontas. No entanto, confiar em exemplos de código prontos que não funcionam para eles (e nesta área, o código escrito há mais de um ano, por padrão, requer atualização ou retrabalho geral, e isso se aplica a código com estouro de pilha, guias de biblioteca , e mesmo guias do próprio Google), eles não entendem os motivos dos erros ocorridos ou o comportamento diferente,desde que conte com uma biblioteca comoSala chinesa , sem tentar compreender a sua arquitectura e princípios de trabalho.





Uma vez que os problemas da visualização do reciclador surgem com frequência, gostaria de entender um pouco sobre como fazer um código extensível e limpo sozinho para exibir uma lista de vários itens em um aplicativo.






Estudando os padrões arquitetônicos de desenvolvimento do Android, treinei-me para primeiro procurar respostas no servidor de guias do desenvolvedor do Google . Mas às vezes lá, especialmente nos codelabs de treinamento, há exemplos de código que são mais simplificados do que projetados para versatilidade, pureza e extensibilidade.





Nesse caso, precisei usar uma visualização sofisticada do reciclador para exibir uma lista de itens com marcações e lógicas internas diferentes. Todos os aplicativos modernos são baseados nesta ideia - de mensageiros instantâneos e feeds de mídia social a aplicativos bancários. Além disso, combinar em tempo real usando uma abordagem reativa de diferentes elementos visuais da lista de visualização do reciclador em vez da marcação de layout manual é uma ponte para o mundo da interface do usuário declarativa-funcional, que é oferecida a nós no Jetpack Compose, e que mais cedo ou mais tarde, o Google gentilmente oferecerá a troca.





Codelab, recycler view , sealed . . , ,- , , . , /, ( , SOLID, ).





, Google id data- : id Long.MIN_VALUE, id data-. : data-, , . recycler view .





. adapter delegates, groupie epoxy. , . , , . , , , .





:





  • , , 10%, ;





  • : , - data- .





, , , , , , .





, , , , recycler view , . , .





, .

recycler view, ListAdapter, , :





  • getItemType - , ( , Google );





  • onCreateViewHolder - , ViewHolder , ( );





  • onBindViewHolder - , ( ) ViewHolder, .





recycler view , recycler view , , , , DiffUtil-.





DiffCallback,
class BaseDiffCallback : DiffUtil.ItemCallback<HasStringId>() {
    override fun areItemsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem == newItem
}
      
      



, areContentsTheSame , areItemsTheSame true. HasStringId, id String equals, data- , view. Data- id, DiffUtil , ui- id .





, , . , :





interface ViewHoldersManager {
    fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor)
    fun getItemType(item: Any): Int
    fun getViewHolder(itemType: Int): ViewHolderVisitor
}
      
      



recycler view:





object ItemTypes {
    const val UNKNOWN = -1
    const val HEADER = 0
    const val TWO_STRINGS = 1
    const val ONE_LINE_STRINGS = 2
    const val CARD = 3
}
      
      



"" adapter delegates, . .





hilt data binding, : ui. , , :





@Module
@InstallIn(FragmentComponent::class)
object DiModule {

    @Provides
    @FragmentScoped
    fun provideAdaptersManager(): ViewHoldersManager = ViewHoldersManagerImpl().apply {
        registerViewHolder(ItemTypes.HEADER, HeaderViewHolder())
        registerViewHolder(ItemTypes.ONE_LINE_STRINGS, OneLine2ViewHolder())
        registerViewHolder(ItemTypes.TWO_STRINGS, TwoStringsViewHolder())
        registerViewHolder(ItemTypes.CARD, CardViewHolder())
    }
}
      
      



:





ard item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable name="card" type="ru.alexmaryin.recycleronvisitor.data.ui_models.CardItem" />
    </data>

    <androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_margin="8dp"
        card_view:cardBackgroundColor="@color/cardview_shadow_end_color"
        card_view:cardCornerRadius="15dp">

        <ImageView
            android:id="@+id/card_background_image"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:scaleType="centerCrop"
            tools:ignore="ContentDescription"
            tools:src="@android:mipmap/sym_def_app_icon" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:background="@android:drawable/screen_background_dark_transparent"
            android:orientation="vertical"
            android:padding="16dp">

            <TextView
                android:id="@+id/card_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:maxLines="1"
                android:paddingTop="8dp"
                android:paddingBottom="8dp"
                android:textAllCaps="true"
                android:textColor="#FFFFFF"
                android:textStyle="bold"
                tools:text="Cart title"
                android:text="@{card.title}"/>

            <TextView
                android:id="@+id/txt_discription"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:maxLines="2"
                android:textColor="#FFFFFF"
                tools:text="this is a simple discription with losts of text lorem ipsum dolor sit amet,
            consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
                android:text="@{card.description}"/>

        </LinearLayout>
    </androidx.cardview.widget.CardView>
</layout>
      
      



One line item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.OneLineItem2" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/text1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:paddingStart="8dp"
            android:text="@{model.left}"
            android:textAlignment="textEnd"
            android:textAppearance="?attr/textAppearanceListItem"
            android:textColor="@color/cardview_dark_background"
            app:layout_constraintEnd_toStartOf="@+id/divider"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:ignore="RtlSymmetry,TextContrastCheck"
            tools:text="Left text" />

        <ImageView
            android:id="@+id/divider"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:alpha="0.6"
            android:padding="5dp"
            android:scaleType="center"
            android:scaleX="0.5"
            android:scaleY="0.9"
            android:src="@drawable/ic_outline_waves_24"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/text1"
            app:layout_constraintEnd_toStartOf="@+id/text2"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/text1"
            app:layout_constraintTop_toTopOf="@+id/text1"
            app:srcCompat="@drawable/ic_outline_waves_24"
            tools:ignore="ContentDescription"
            tools:visibility="visible" />

        <TextView
            android:id="@id/text2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:paddingEnd="8dp"
            android:text="@{model.right}"
            android:textAppearance="?attr/textAppearanceListItem"
            app:layout_constraintBottom_toBottomOf="@+id/divider"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/divider"
            app:layout_constraintTop_toTopOf="@+id/divider"
            tools:ignore="RtlSymmetry"
            tools:text="Right text" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
      
      



Two line item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.TwoStringsItem" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="?attr/listPreferredItemHeight"
        android:mode="twoLine"
        android:paddingStart="?attr/listPreferredItemPaddingStart"
        android:paddingEnd="?attr/listPreferredItemPaddingEnd">

        <TextView
            android:id="@+id/text1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="@{model.caption}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:textAppearance="?attr/textAppearanceListItem" />

        <TextView
            android:id="@id/text2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{model.details}"
            app:layout_constraintTop_toBottomOf="@id/text1"
            app:layout_constraintStart_toStartOf="parent"
            android:textAppearance="?attr/textAppearanceListItemSecondary" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

      
      



Header item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="headerItem"
            type="ru.alexmaryin.recycleronvisitor.data.ui_models.RecyclerHeader" />
    </data>

    <TextView
        style="@style/regularText"
        android:id="@+id/header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#591976D2"
        android:textAlignment="center"
        android:textStyle="italic"
        android:text="@{headerItem.text}"/>
</layout>
      
      



, :





interface ViewHolderVisitor {
    val layout: Int
    fun acceptBinding(item: Any): Boolean
    fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById)
}
      
      



( acceptVisitor execute, , ) - acceptBinding bind, layout, .





accept : ( ) , , , accept, , true. , , , . - (accept = true), - , .





, , . :





class ViewHoldersManagerImpl : ViewHoldersManager {

    private val holdersMap = emptyMap<Int, ViewHolderVisitor>().toMutableMap()

    override fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor) {
        holdersMap += itemType to viewHolder
    }

    override fun getItemType(item: Any): Int {
        holdersMap.forEach { (itemType, holder) -> 
            if(holder.acceptBinding(item)) return itemType
        }
        return ItemTypes.UNKNOWN
    }

    override fun getViewHolder(itemType: Int) = holdersMap[itemType] ?: throw TypeCastException("Unknown recycler item type!")
}
      
      



( ):





class CardViewHolder : ViewHolderVisitor {
  
    override val layout: Int = R.layout.card_item

    override fun acceptBinding(item: Any): Boolean = item is CardItem

    override fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById) {
        with(binding as CardItemBinding) {
            card = item as CardItem
            Picasso.get().load(item.image).into(cardBackgroundImage)
        }
    }
}
      
      



as . -, , : accept , CardItem, bind . : layout, binding data binding . -, , idea android studio ?





, recycler view,- , , , :





class BaseListAdapter(
    private val clickListener: AdapterClickListenerById,
    private val viewHoldersManager: ViewHoldersManager
) : ListAdapter<HasStringId, BaseListAdapter.DataViewHolder>(BaseDiffCallback()) {

    inner class DataViewHolder(
        private val binding: ViewDataBinding,
        private val holder: ViewHolderVisitor
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item: HasStringId, clickListener: AdapterClickListenerById) =
            holder.bind(binding, item, clickListener)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder =
        LayoutInflater.from(parent.context).run {
            val holder = viewHoldersManager.getViewHolder(viewType)
            DataViewHolder(DataBindingUtil.inflate(this, holder.layout, parent, false), holder)
        }

    override fun onBindViewHolder(holder: DataViewHolder, position: Int) = holder.bind(getItem(position), clickListener)

    override fun getItemViewType(position: Int): Int = viewHoldersManager.getItemType(getItem(position))
}
      
      



view, :





// -   :
// private val viewModel: MainViewModel by viewModels()
// private lateinit var recycler: RecyclerView
// @Inject lateinit var viewHoldersManager: ViewHoldersManager
// private val items = mutableListOf<HasStringId>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        recycler = requireActivity().findViewById(R.id.recycller)
        val itemsAdapter = BaseListAdapter(AdapterClickListenerById {}, viewHoldersManager)
        itemsAdapter.submitList(items)
        recycler.apply {
            layoutManager = LinearLayoutManager(requireContext())
            addItemDecoration(DividerItemDecoration(requireContext(), (layoutManager as LinearLayoutManager).orientation))
            adapter = itemsAdapter
        }
        populateRecycler()
    }

private fun populateRecycler() {
     lifecycleScope.launch {
        viewModel.getItems().flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
           .collect { items.add(it) }
     }
   }
      
      



"" , recycler view . :





  • -;





  • sealed ;





  • data- / , view data ;





  • - ;





  • , SOLID ;





  • , (YAGNI).





Claro, minha implementação ainda tem maneiras de melhorar e expandir. Você pode, como em groupie, adicionar agrupamento de elementos e seu colapso visual. Você pode abandonar a vinculação de dados ou complementar o adaptador com opções para uma vinculação de visão ou um aumento de marcação regular com todos os seus findViewById favoritos em suportes de visão. E então o código se transformará na mesma biblioteca, da qual já existem tantos e assim. Para os meus propósitos específicos, no momento em que surgiu a necessidade, a opção com um simples Visitante é mais do que suficiente:





Por favor, não julgue estritamente, já que este é meu primeiro nascimento no mundo andróide. O código de exemplo completo do texto do artigo estará disponível no repositório github .








All Articles