Une version simple d'une vue de recycleur hétéroclite sur le modèle de visiteur

Six mois se sont écoulés depuis que j'ai roulé de Pascalsur kotlin et je suis tombé amoureux du développement Android, et maintenant je me permets déjà de grimper publiquement avec mes idées dans le monastère de quelqu'un d'autre. Mais il y a une raison à cela. Après avoir regardé dans les discussions de profil quelles questions se posent le plus souvent pour les développeurs Android, et pas seulement pour les débutants, je me suis rendu compte que dans la plupart des cas, lorsqu'une personne rencontre une erreur qu'elle ne peut pas comprendre, car elle ne peut pas comprendre l'explication des collègues du chat ou leurs principales questions, la raison en est l'utilisation irréfléchie de morceaux de code ou de bibliothèques prêts à l'emploi. Cependant, en s'appuyant sur des exemples de code prêts à l'emploi qui ne fonctionnent pas pour eux (et dans ce domaine, le code écrit il y a plus d'un an, par défaut, nécessite une mise à jour ou généralement un remaniement, et cela s'applique au code avec débordement de pile, guides de bibliothèque , et même des guides de Google lui-même), ils ne comprennent pas les raisons des erreurs ou les comportements différents,depuis s'appuyer sur une bibliothèque commeSalle chinoise , sans chercher à comprendre son architecture et ses principes de travail.





Étant donné que les problèmes d'affichage des recycleurs apparaissent très souvent, j'aimerais comprendre un peu comment créer moi-même un code extensible et propre pour afficher une liste multi-éléments dans une application.






En étudiant les modèles architecturaux du développement Android, je me suis formé à la première recherche de réponses sur le serveur des guides de développement Google . Mais parfois là-bas, en particulier dans les ateliers de codage de formation, il existe des exemples de code plus simplifiés que conçus pour la polyvalence, la pureté et l'extensibilité.





Dans ce cas, j'avais besoin d'utiliser une vue de recycleur sophistiquée pour afficher une liste d'éléments avec un balisage et une logique internes différents. Toutes les applications modernes sont basées sur cette idée - des messageries instantanées et des flux de médias sociaux aux applications bancaires. De plus, combiner à la volée en utilisant une approche réactive de différents éléments visuels de la liste des vues du recycleur au lieu du balisage de mise en page manuel est un pont vers le monde de l'interface utilisateur déclarative-fonctionnelle, qui nous est offerte dans Jetpack Compose, et qui plus tôt ou plus tard, Google proposera doucement de basculer.





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





Bien sûr, ma mise en œuvre a encore des moyens de s'améliorer et de s'étendre. Vous pouvez, comme dans groupie, ajouter un regroupement d'éléments et leur réduction visuelle. Vous pouvez abandonner la liaison de données ou compléter l'adaptateur avec des options pour une liaison de vue ou un gonflage de balisage régulier avec tous vos findViewById préférés dans les supports de vue. Et puis le code se transformera en la même bibliothèque, dont il y en a déjà tellement et ainsi de suite. Pour mes besoins spécifiques, au moment où le besoin se fait sentir, l'option avec un simple Visiteur est plus que suffisante:





Ne jugez pas strictement, car c'est ma première naissance dans le monde androïde. L'exemple de code complet du texte de l'article sera disponible dans le référentiel github .








All Articles