
Ilya Nekrasov, Mahtalitet , Android-Entwickler von KODE
Zweieinhalb Jahre lang habe ich in der Android-Entwicklung an völlig anderen Projekten gearbeitet: von einem sozialen Netzwerk für Autofahrer und eine lettische Bank über ein Bundesbonussystem bis hin zur dritten Fluggesellschaft für den Transport. Wie auch immer, in jedem dieser Projekte stieß ich auf Aufgaben, die die Suche nach nicht klassischen Lösungen bei der Implementierung von Listen mit der RecyclerView-Klasse erforderten.
Dieser Artikel - das Ergebnis der Vorbereitung auf eine Aufführung beim DevFest Kaliningrad'18 sowie der Kommunikation mit Kollegen - ist besonders nützlich für Anfänger und diejenigen, die nur eine der vorhandenen Bibliotheken verwendet haben.
Zunächst beschäftigen wir uns ein wenig mit der Essenz des Problems und der Schmerzquelle, nämlich dem Wachstum der Funktionalität bei der Entwicklung der Anwendung und der Komplexität der verwendeten Listen.
Kapitel eins, in dem der Kunde von einer Anwendung träumt und wir - über klare Anforderungen
Stellen wir uns eine Situation vor, in der ein Kunde ein Studio kontaktiert, das eine mobile Anwendung für ein Gummiente-Geschäft wünscht.
Das Projekt entwickelt sich rasant, es entstehen regelmäßig neue Ideen, die nicht in eine langfristige Roadmap eingebettet sind.
Zunächst fordert uns der Kunde auf, eine Liste der vorhandenen Waren anzuzeigen und beim Klicken eine Lieferanfrage auszufüllen. Sie müssen nicht weit gehen, um eine Lösung zu finden: Wir verwenden das klassische Set von RecyclerView , einen einfachen selbstgeschriebenen Adapter dafür und Activity .
Für den Adapter verwenden wir homogene Daten, einen ViewHolder und eine einfache Bindungslogik.
Adapter mit Entenclass DucksClassicAdapter( private val data: List<Duck>, private val onDuckClickAction: (Duck) -> Unit ) : RecyclerView.Adapter<DucksClassicAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val rubberDuckView = LayoutInflater.from(parent.context).inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(rubberDuckView) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val duck = data[position] holder.divider.isVisible = position != 0 holder.rubberDuckImage.apply { Picasso.get() .load(duck.icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(R.drawable.duck_stub) .into(this) } holder.clicksHolder.setOnClickListener { onDuckClickAction.invoke(duck) } } override fun getItemCount() = data.count() class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val clicksHolder: View = view.findViewById(R.id.clicksHolder) val divider: View = view.findViewById(R.id.divider) } }
Im Laufe der Zeit hat der Kunde die Idee, den Gummienten eine weitere Warengruppe hinzuzufügen, was bedeutet, dass er ein neues Datenmodell und ein neues Layout hinzufügen muss. Am wichtigsten ist jedoch, dass im Adapter ein anderer ViewType angezeigt wird, mit dem Sie festlegen können, welcher ViewHolder für ein bestimmtes Listenelement verwendet werden soll.
Danach werden den Kategorien Überschriften hinzugefügt, nach denen jede Kategorie reduziert und erweitert werden kann, um die Ausrichtung der Benutzer im Geschäft zu vereinfachen. Dies ist plus ein weiterer ViewType und ViewHolder für Header. Außerdem müssen Sie den Adapter komplizieren, da Sie eine Liste offener Gruppen führen und damit überprüfen müssen, ob dieses oder jenes Element durch Klicken auf die Kopfzeile ausgeblendet und angezeigt werden muss.
Adapter mit allem class DucksClassicAdapter( private val onDuckClickAction: (Pair<Duck, Int>) -> Unit, private val onSlipperClickAction: (Duck) -> Unit, private val onAdvertClickAction: (Advert) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var data: List<Duck> = emptyList() set(value) { field = value internalData = data.groupBy { it.javaClass.kotlin } .flatMap { groupedDucks -> val titleRes = when (groupedDucks.key) { DuckSlipper::class -> R.string.slippers RubberDuck::class -> R.string.rubber_ducks else -> R.string.mixed_ducks } groupedDucks.value.let { listOf(FakeDuck(titleRes, it)).plus(it) } } .toMutableList() duckCountsAdapters = internalData.map { duck -> val rubberDuck = (duck as? RubberDuck) DucksCountAdapter( data = (1..(rubberDuck?.count ?: 1)).map { count -> duck to count }, onCountClickAction = { onDuckClickAction.invoke(it) } ) } } private val advert = DuckMockData.adverts.orEmpty().shuffled().first() private var internalData: MutableList<Duck> = mutableListOf() private var duckCountsAdapters: List<DucksCountAdapter> = emptyList() private var collapsedHeaders: MutableSet<Duck> = hashSetOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_RUBBER_DUCK -> { val view = parent.context.inflate(R.layout.item_rubber_duck, parent) DuckViewHolder(view) } VIEW_TYPE_SLIPPER_DUCK -> { val view = parent.context.inflate(R.layout.item_duck_slipper, parent) SlipperViewHolder(view) } VIEW_TYPE_HEADER -> { val view = parent.context.inflate(R.layout.item_header, parent) HeaderViewHolder(view) } VIEW_TYPE_ADVERT -> { val view = parent.context.inflate(R.layout.item_advert, parent) AdvertViewHolder(view) } else -> throw UnsupportedOperationException("view type $viewType without ViewHolder") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is HeaderViewHolder -> bindHeaderViewHolder(holder, position) is DuckViewHolder -> bindDuckViewHolder(holder, position) is SlipperViewHolder -> bindSlipperViewHolder(holder, position) is AdvertViewHolder -> bindAdvertViewHolder(holder) } } private fun bindAdvertViewHolder(holder: AdvertViewHolder) { holder.advertImage.showIcon(advert.icon) holder.advertTagline.text = advert.tagline holder.itemView.setOnClickListener { onAdvertClickAction.invoke(advert) } } private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { val item = getItem(position) as FakeDuck holder.clicksHolder.setOnClickListener { changeCollapseState(item, position) } val arrowRes = if (collapsedHeaders.contains(item)) R.drawable.ic_keyboard_arrow_up_black_24dp else R.drawable.ic_keyboard_arrow_down_black_24dp holder.arrow.setImageResource(arrowRes) holder.title.setText(item.titleRes) } private fun changeCollapseState(item: FakeDuck, position: Int) { val isCollapsed = collapsedHeaders.contains(item) if (isCollapsed) { collapsedHeaders.remove(item) } else { collapsedHeaders.add(item) }
Ich denke, Sie verstehen es - ein solcher kleiner Haufen ähnelt einer gesunden Entwicklung. Und im Voraus gibt es immer mehr Anforderungen an den Kunden: ein Werbebanner ganz oben auf der Liste zu platzieren, die Möglichkeit zu erkennen, die Anzahl der bestellten Enten zu wählen. Nur diese Aufgaben werden letztendlich zu regulären Adaptern, die wiederum von Grund auf neu geschrieben werden müssen.
Der Prozess der Entwicklung eines klassischen Adapters in der Geschichte von Github
Zusammenfassung
Tatsächlich ist das Bild überhaupt nicht ermutigend: Einzelne Adapter müssen für bestimmte Fälle geschärft werden. Wir alle wissen, dass es in einer realen Anwendung Dutzende oder sogar Hunderte solcher Listenbildschirme gibt. Und sie enthalten keine Informationen über Enten, sondern komplexere Daten. Ja, und ihr Design ist viel komplizierter.
Was ist los mit unseren Adaptern?
- offensichtlich sind sie schwer wiederzuverwenden;
- innerhalb der Geschäftslogik erscheint und im Laufe der Zeit wird es immer mehr;
- schwer zu warten und zu erweitern;
- hohes Fehlerrisiko beim Aktualisieren von Daten;
- nicht offensichtliches Design.
Kapitel zwei, in dem alles anders sein könnte
Es ist unrealistisch und sinnlos, sich die Entwicklung der Anwendung für die kommenden Jahre vorzustellen. Nach ein paar solchen Tänzen mit einem Tamburin wie im letzten Kapitel und dem Schreiben von Dutzenden von Adaptern wird jeder die Frage haben: „Vielleicht gibt es andere Lösungen?“.

Nach Abschluss von Github haben wir festgestellt , dass die erste AdapterDelegates- Bibliothek im Jahr 2015 veröffentlicht wurde, und ein Jahr später haben Groupie und Epoxy das Arsenal der Entwickler erweitert - sie alle tragen zur Erleichterung des Lebens bei, aber jede hat ihre eigenen Besonderheiten und Fallstricke.
Es gibt mehrere ähnliche Bibliotheken (z. B. FastAdapter), aber weder ich noch meine Kollegen haben mit ihnen gearbeitet, sodass wir sie im Artikel nicht berücksichtigen werden.
Bevor wir die Bibliotheken vergleichen, analysieren wir kurz den oben beschriebenen Fall mit dem Online-Shop unter der Bedingung, dass AdapterDelegates verwendet wird. Von den zerlegten Bibliotheken ist er aus Sicht der internen Implementierung und Verwendung am einfachsten (er ist jedoch nicht in allen Bereichen fortgeschritten, sodass Sie viele Dinge von Hand hinzufügen müssen).
Die Bibliothek wird uns nicht vollständig aus dem Adapter retten, sondern aus Blöcken (Bausteinen) bestehen, die wir sicher zur Liste hinzufügen oder daraus entfernen und austauschen können.
Blockadapter class DucksDelegatesAdapter : ListDelegationAdapter<List<DisplayableItem>>() { init { delegatesManager.addDelegate(RubberDuckDelegate()) } fun setData(items: List<DisplayableItem>) { this.items = items notifyDataSetChanged() } } private class RubberDuckDelegate : AbsListItemAdapterDelegate<RubberDuckItem, DisplayableItem, RubberDuckDelegate.ViewHolder>() { override fun isForViewType(item: DisplayableItem, items: List<DisplayableItem>, position: Int): Boolean { return item is RubberDuckItem } override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { val item = parent.context.inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(item) } override fun onBindViewHolder(item: RubberDuckItem, viewHolder: ViewHolder, payloads: List<Any>) { viewHolder.apply { rubberDuckImage.showIcon(item.icon) } } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val rubberDuckImage: ImageView = itemView.findViewById(R.id.rubberDuckImage) } }
Verwenden eines Aktivitätsadapters class DucksDelegatesActivity : AppCompatActivity() { private lateinit var ducksList: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ducks_delegates) ducksList = findViewById(R.id.duckList) ducksList.apply { layoutManager = LinearLayoutManager(this@DucksDelegatesActivity) adapter = createAdapter().apply { showData() } } } fun createAdapter(): DucksDelegatesAdapter { return DucksDelegatesAdapter() } private fun DucksDelegatesAdapter.showData() { setData(getRubberDucks()) } private fun getRubberDucks(): List<DisplayableItem> { return DuckMockData.ducks.orEmpty().map { RubberDuckItem(it.icon) } } }
Ab der ersten Aufgabe sehen wir den Unterschied: Wir haben eine Adapterklasse, die von der Bibliothek erbt. Und außerdem - derselbe Baustein, der als Delegat bezeichnet wird und von dem wir auch einen Teil der Logik erben und implementieren, die wir benötigen. Als nächstes fügen wir dem Manager einen Delegaten hinzu - dies ist auch eine Bibliotheksklasse. Und das Letzte, was Sie brauchen, ist, einen Adapter zu erstellen und ihn mit Daten zu füllen
Um die zweite Kategorie des Geschäfts und der Header zu implementieren, werden wir ein paar weitere Delegaten schreiben, und die Animation wird dank der DiffUtil- Klasse angezeigt .
Hier möchte ich eine kurze, aber endgültige Schlussfolgerung ziehen: Die Verwendung selbst dieser Bibliothek löst alle aufgeführten Probleme, die aufgetreten sind, als wir die Anwendung in einem Fall mit einem Online-Shop kompliziert haben, aber nirgendwo anders ohne die Minuspunkte.
Adapterentwicklungsprozess mit AdapterDelegates im Github- Verlauf
Kapitel 3, in dem der Entwickler die rosa Brille durch Vergleichen der Bibliotheken entfernt
Wir werden detaillierter auf die Funktionalität und den Betrieb jeder der Bibliotheken eingehen. Auf die eine oder andere Weise habe ich alle drei Bibliotheken in unseren Projekten verwendet, abhängig von den Aufgaben und der Komplexität der Anwendung.
Wir verwenden diese Bibliothek für die Anwendung einer der größten russischen Fluggesellschaften. Wir mussten eine einfache Zahlungsliste durch eine Liste mit Gruppen und einer großen Anzahl verschiedener Parameter ersetzen.
Ein vereinfachtes Bibliotheksarbeitsschema sieht folgendermaßen aus: 
Die Hauptklasse ist der DelegateAdapter , die verschiedenen "Bausteine" sind die "Delegaten", die für die Anzeige eines bestimmten Datentyps und natürlich der Liste selbst verantwortlich sind.
Vorteile:
- Einfachheit des Eintauchens;
- einfach wiederverwendbare Adapter;
- wenige Methoden und Klassen;
- Keine Reflexion, Codegenerierung und Datenbindung.
Nachteile:
- Sie müssen die Logik beispielsweise selbst implementieren
Aktualisieren von Elementen über DiffUti l (seit Version 3.1.0 können Sie den AsyncListDifferDelegationAdapter-Adapter verwenden); - redundanter Code.
Im Allgemeinen löst diese Bibliothek alle Hauptschwierigkeiten bei der Erweiterung der Funktionalität der Anwendung und ist für diejenigen geeignet, die die Bibliothek zuvor noch nicht verwendet haben. Aber um nur damit aufzuhören, rate ich nicht.
Wir verwenden häufig den Groupie , der vor einigen Jahren von Lisa Wray erstellt wurde , einschließlich des Schreibens des Antrags für eine lettische Bank, der ihn vollständig verwendet.
Um diese Bibliothek nutzen zu können, müssen Sie sich zunächst mit Abhängigkeiten befassen . Zusätzlich zur Hauptoption können Sie aus mehreren Optionen auswählen:
Wir bleiben bei einer Sache und schreiben die notwendigen Abhängigkeiten vor.
Anhand eines Beispiels eines Online-Shops mit Enten müssen wir ein von der Bibliotheksklasse geerbtes Element erstellen, das Layout angeben und die Bindung über die Kotlin-Syntentik implementieren. Verglichen mit der Menge an Code, die ich mit AdapterDelegates schreiben musste , ist es nur Himmel und Erde.
Sie müssen lediglich den RecyclerView GroupieAdapter als Adapter festlegen und die verspotteten Elemente darin ablegen .

Es ist ersichtlich, dass das Arbeitsschema größer und komplexer ist. Hier können Sie neben einfachen Elementen auch ganze Abschnitte verwenden - Gruppen von Elementen und andere Klassen.
Vorteile:
- intuitive Benutzeroberfläche, obwohl API Sie zum Nachdenken anregt;
- das Vorhandensein von Boxlösungen;
- Aufteilung in Gruppen von Elementen;
- die Wahl zwischen der üblichen Option, Kotlin Extensions und DataBinding;
- Einbetten von ItemDecoration und Animationen.
Nachteile:
- unvollständiges Wiki;
- schlechte Unterstützung durch den Betreuer und die Gemeinde;
- kleine Fehler, die in der ersten Version umgangen werden mussten;
- im Hauptfaden unterschiedlich (vorerst);
- Es gibt keine Unterstützung für AndroidX (im Moment müssen Sie das Repository überwachen).
Es ist wichtig, dass Groupie mit all seinen Nachteilen AdapterDelegates problemlos ersetzen kann , insbesondere wenn Sie Faltlisten der ersten Ebene erstellen möchten und nicht viel Boilerplate schreiben möchten.
Implementierung der Entenliste mit Groupie
Die letzte Bibliothek, die wir vor relativ kurzer Zeit verwenden, ist Epoxy , das von den Airbnb- Leuten entwickelt wurde. Die Bibliothek ist komplex, aber Sie können eine ganze Reihe von Aufgaben lösen. Airbnb-Programmierer selbst verwenden es, um Bildschirme direkt vom Server zu rendern. Epoxy hat sich bei einem unserer neuesten Projekte als nützlich erwiesen - einer Bewerbung für eine Bank in Jekaterinburg.
Um Bildschirme zu entwickeln, mussten wir mit verschiedenen Datentypen arbeiten, einer großen Anzahl von Listen. Und einer der Bildschirme war wirklich endlos. Und Epoxy hat uns allen dabei geholfen.
Das Prinzip der Bibliothek als Ganzes ähnelt den beiden vorherigen, außer dass anstelle eines Adapters EpoxyController zum Erstellen der Liste verwendet wird, mit dem Sie die Struktur des Adapters deklarativ bestimmen können.

Um dies zu erreichen, basiert die Bibliothek auf der Codegenerierung. Wie es funktioniert - mit all den Nuancen ist es im Wiki gut beschrieben und spiegelt sich in den Beispielen wider.
Vorteile:
- Modelle für die Liste, generiert aus der üblichen Ansicht, mit der Möglichkeit der Wiederverwendung in einfachen Bildschirmen;
- deklarative Beschreibung der Bildschirme;
- Datenbindung mit maximaler Geschwindigkeit - generiert Modelle direkt aus Layoutdateien;
- Zeigen Sie einfach nicht nur Listen aus den Blöcken an, sondern auch komplexe Bildschirme.
- freigegebener ViewPool bei Aktivität;
- asynchrones Diffing out of the Box (AsyncEpoxyController);
- Keine Notwendigkeit, mit horizontalen Listen zu dämpfen.
Nachteile:
- viele Klassen, Prozessoren, Anmerkungen;
- schwieriger Tauchgang von Grund auf neu;
- verwendet das ButterKnife-Plugin, um R2-Dateien in Modulen zu generieren;
- Es ist sehr schwierig zu verstehen, wie man mit Rückrufen richtig arbeitet (wir selbst haben es noch nicht verstanden).
- Es gibt Probleme, die umgangen werden müssen: Zum Beispiel Absturz mit derselben ID
Implementierung der Entenliste mit Epoxy
Zusammenfassung
Die Hauptsache, die ich vermitteln wollte, war: Lassen Sie sich nicht mit der Komplexität abfinden, die auftritt, wenn Sie komplexe Listen erstellen und diese ständig wiederholen müssen. Und das passiert sehr oft. Und im Prinzip, wenn sie implementiert werden, wenn das Projekt gerade erst beginnt oder Sie sich mit dessen Umgestaltung befassen.
Die Realität ist, dass es nicht notwendig ist, die Logik noch einmal zu komplizieren, da wir glauben, dass eine Art von eigenen Abstraktionen ausreichen wird. Sie sind kurz genug und die Arbeit mit ihnen ist nicht nur kein Vergnügen, sondern es besteht auch die Versuchung, einen Teil der Logik auf den UI-Teil zu übertragen, der nicht vorhanden sein sollte. Es gibt Tools, mit denen Sie die meisten Probleme vermeiden können und die Sie verwenden müssen.
Ich verstehe, dass dies für viele erfahrene (und nicht nur) Entwickler entweder offensichtlich ist oder sie mir nicht zustimmen. Ich halte es jedoch für wichtig, dies noch einmal zu betonen.
Also, was soll ich wählen?
Die Empfehlung, in einer Bibliothek zu bleiben, ist ziemlich schwierig, da die Wahl von vielen Faktoren abhängt: von persönlichen Vorlieben bis zur Ideologie des Projekts.
Ich würde folgendes tun:
- Wenn Sie gerade erst mit der Entwicklung beginnen, versuchen Sie, mit AdapterDelegates ein kleines Projekt zu starten - dies ist die einfachste Bibliothek - es sind keine besonderen Kenntnisse erforderlich. Sie werden verstehen, wie man damit arbeitet und warum es bequemer ist, als selbst Adapter zu schreiben.
- Groupie eignet sich für diejenigen, die bereits genug mit AdapterDelegates gespielt haben und es satt haben, ein paar Boilerplates zu schreiben, oder für alle anderen, die sofort mit einem Mittelweg beginnen möchten. Und vergessen Sie nicht, dass Gruppen aus dem Kasten heraus gefaltet werden - dies ist auch ein gutes Argument für sie.
- Nun, und Epoxy - für diejenigen, die mit einem wirklich komplexen Projekt mit einer großen Datenmenge konfrontiert sind, ist die Komplexität mit einer Fettbibliothek weniger problematisch. Zuerst wird es schwierig sein, aber dann wird die Implementierung der Listen wie eine Kleinigkeit erscheinen. Ein wichtiges Argument für Epoxy könnte das Vorhandensein von DataBinding und MVVM im Projekt sein - es wurde buchstäblich dafür erstellt, da die Möglichkeit besteht, Modelle aus den entsprechenden Layouts zu generieren.
Wenn Sie noch Fragen haben, können Sie über den Link den Code unserer Anwendung mit den Details erneut anzeigen.