Eine einfache Version einer bunten Recycler-Ansicht in der Besuchervorlage

Sechs Monate sind vergangen, seit ich von Pascal gerollt binauf kotlin und verliebte sich in die Android-Entwicklung, und jetzt erlaube ich mir bereits, öffentlich mit meinen Ideen in das Kloster eines anderen zu klettern. Dafür gibt es aber einen Grund. Nachdem ich in Profil-Chats beobachtet hatte, welche Fragen für Android-Entwickler und nicht nur für Anfänger am häufigsten auftauchen, wurde mir klar, dass eine Person in den meisten Fällen auf einen Fehler stößt, den sie nicht verstehen kann, da sie die Erklärung der Kollegen aus dem Chat oder nicht verstehen kann Der Grund für ihre Hauptfragen ist die gedankenlose Verwendung von vorgefertigten Codeteilen oder Bibliotheken. Wenn Sie sich jedoch auf vorgefertigte Codebeispiele verlassen, die für sie nicht funktionieren (und in diesem Bereich muss der vor mehr als einem Jahr geschriebene Code standardmäßig aktualisiert oder allgemein überarbeitet werden. Dies gilt für Code mit Stapelüberlauf und Bibliothekshandbüchern und sogar Anleitungen von Google selbst) verstehen sie die Gründe für die auftretenden Fehler oder das unterschiedliche Verhalten nicht.da verlassen sie sich auf eine bibliothek wieChinesisches Zimmer , ohne zu versuchen, seine Architektur und Arbeitsprinzipien zu verstehen.





Da Probleme mit der Recycler-Ansicht sehr häufig auftreten, möchte ich ein wenig verstehen, wie Sie selbst einen erweiterbaren und sauberen Code erstellen, um eine Liste mit mehreren Elementen in einer Anwendung anzuzeigen.






Ich studierte die Architekturmuster der Android-Entwicklung und trainierte mich, zunächst auf dem Server der Google Developer Guides nach Antworten zu suchen . Aber manchmal, insbesondere in den Trainings-Codelabs, gibt es Beispiele für Code, die einfacher sind als auf Vielseitigkeit, Reinheit und Erweiterbarkeit ausgelegt.





In diesem Fall musste ich eine ausgefallene Recycler-Ansicht verwenden, um eine Liste von Elementen mit unterschiedlichen internen Markups und Logik anzuzeigen. Alle modernen Anwendungen basieren auf dieser Idee - von Instant Messenger und Social Media-Feeds bis hin zu Bankanwendungen. Darüber hinaus ist das spontane Kombinieren mit einem reaktiven Ansatz verschiedener visueller Elemente der Recycler-Ansichtsliste anstelle eines manuellen Layout-Markups eine Brücke in die Welt der deklarativ-funktionalen Benutzeroberfläche, die uns in Jetpack Compose angeboten wird und die früher oder früher angeboten wird später wird Google sanft anbieten, zu wechseln.





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





Natürlich hat meine Implementierung noch Möglichkeiten zur Verbesserung und Erweiterung. Sie können wie in Groupie die Gruppierung von Elementen und deren visuellen Zusammenbruch hinzufügen. Sie können die Datenbindung aufgeben oder den Adapter mit Optionen für eine Ansichtsbindung oder ein regelmäßiges Aufblasen von Markups mit all Ihren bevorzugten findViewById in Ansichtshaltern ergänzen. Und dann verwandelt sich der Code in dieselbe Bibliothek, von der es bereits so viele und so gibt. Für meine speziellen Zwecke ist die Option mit einem einfachen Besucher in dem Moment, in dem sich die Notwendigkeit ergab, mehr als ausreichend:





Bitte urteilen Sie nicht streng, da dies meine erste Geburt in der Android-Welt ist. Der vollständige Beispielcode aus dem Text des Artikels ist im Github-Repository verfügbar .








All Articles