Listen werden in so gut wie jeder Android App genutzt. Diese können potenziell viele unterschiedliche List Items enthalten, welche in einer gemeinsamen Liste miteinander kombiniert werden. Wir haben das Ziel unsere bestehende App und deren Legacy UI nach und nach auf Compose umzustellen. Wir brauchen einen Weg, um RecyclerView und Jetpack Compose zu verheiraten.
Inhaltsangabe
Legacy RecyclerView und Jetpack Compose Inhalte
Oftmals tritt der Fall ein, dass wir zu einer bestehenden Liste ein neues Item hinzufügen wollen, das wir (ganz fortschrittlich) mit Compose umsetzen möchten. Unser Ziel ist es nicht die ganze Liste, mit potentiell Dutzenden unterschiedlicher List Item Layouts auf einen Sitz umzubauen, sondern nur ein neues Composable hinzuzufügen oder ein bestehendes List Item damit zu ersetzen.
Das Einführen neuer List Items in Compose, oder das schrittweise Umbauen unserer RecyclerView Inhalte hin zu Jetpack Compose ist hierbei stets „projektverträglicher“ als eine „Big Bang“ Migration. Gerade, wenn wir uns mit dem Umbau komplexer Listen, mit vielen unterschiedlichen Typen von Listen-Inhalten beschäftigen müssen.
Eine typische Listen-Implementierung Im View System besteht aus einer RecyclerView, einem zugehörigen Adapter und ViewHoldern für das ViewBinding. In diesem Artikel werden wir alle nötigen Erweiterungen in den beteiligten Code-Anteilen exemplarisch und Schritt für Schritt beleuchten.
Einen neuen ViewType einführen
Als ersten Schritt, um Composable in einer RecyclerView zu integrieren, führen wir ein Enum ein, um zwischen altem und neuem List Item in unserem bestehenden Adapter zu unterscheiden:
// An enum to differentiate in between legacy and new browser list items
enum class MediaBrowserType {
Legacy,
NewFeature
}
Anschließend überschreiben wir getItemViewType in unserem Adapter, um die Entscheidung zwischen unseren Ausprägungen anhand unserer Daten zu treffen:
class MediaBrowserAdapter(private val items: List<MediaBrowserItem>) : RecyclerView.Adapter<MediaBrowserAdapter.BrowserItemViewHolder>() {
// ...
override fun getItemViewType(position: Int): Int {
return if (/* Decide in between new or legacy list item */) {
MediaBrowserType.NewFeature.ordinal
} else {
MediaBrowserType.Legacy.ordinal
}
}
}
In komplexen Listen bestehen in der Regel bereits viele unterschiedliche List-Item Typen. Hier müssen wir lediglich bestehende Enums und Unterscheidungen entsprechend erweitern. Der zusätzliche Code für unsere Jetpack Compose Integration und das Einfügen von Composable in einer RecyclerView ist minimal.
Unterscheidung zwischen ViewHoldern
RecyclerView und deren Adapter nutzen ViewHolder, um teure View Instanzen wiederzuverwenden. Die Idee hinter diesem Konzept ist, dass View Instanzen möglichst wiederverwendet werden und lediglich durch ViewBinding mit neuen Anzeige-Daten befüttert werden.
Auch wenn wir in Compose nicht mit ViewBinding arbeiten, müssen wir einen zusätzlichen ViewHolder für unsere Jetpack Compose Integration in der RecyclerView erstellen, um kompatibel zu den RecyclerView APIs zu bleiben.
In der Adapter Methode onCreateViewHolder nutzen wir unseren eingeführten ViewType, um einen neuen ViewHolder zu instanziieren, um unser neues List Item anzeigen zu können:
class MediaBrowserAdapter(private val items: List<MediaBrowserItem>) : RecyclerView.Adapter<MediaBrowserAdapter.BrowserItemViewHolder>() {
// ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BrowserItemViewHolder {
return when (viewType) {
MediaBrowserType.Legacy.ordinal -> /* legacy ViewHolder */
// New ViewHolder containing our Compose content
else -> NewBrowserItemViewHolder(ComposeView(parent.context))
}
}
}
In dem obigen Beispiel sieht man, wie simpel unser neuer ViewHolder für unsere neuen Compose Inhalte erzeugt werden kann: Wir brauchen kein zusätzliches „Dummy“-Layout, sondern erzeugen uns lediglich eine neue ComposeView für den notwendigen itemView Parameter eines ViewHolders.
Binding innerhalb eines Compose ViewHolder
Das noch verbleibende Binding in unserem neuen Compose ViewHolder enthält keine Überraschungen, sondern funktioniert wie bei jeder gewöhnlichen Integration einer ComposeView:
// A brand new ViewHolder for our Compose content
class NewBrowserItemViewHolder(private val composeView: ComposeView) : BrowserItemViewHolder(composeView) {
override fun bind(item: MediaBrowserItem) {
// Bind our data by setting content within our ComposeView
composeView.setContent {
AppTheme {
MediaBrowserListItem(
modifier = Modifier.fillMaxWidth(),
item = item
)
}
}
}
}
Das Auslagern unserer neuen List Item Implementierung in ein dediziertes Composable macht den zusätzliche Code in unserem bestehenden Adapter noch überschaubarer.
Notwendige Performance Maßnahmen
Für eine performante Integration von Composable in einer RecyclerView gibt es in neueren Versionen der Compose und RecyclerView Libraries keine weiteren Besonderheiten zu beachten. Mehr Details hierzu finden sich in diesem ausführlichen Blog Post.
Fazit: Neue Compose List Items lassen sich leicht integrieren
Das Beispiel in diesem Artikel zeigt wie wenig zusätzlicher Code notwendig ist, um Composable in einer RecyclerView einzufügen. In dem bereits notwendigen und in der Regel sehr umfangreichen Boiler-Plate-Code einer Listen Implementierung mittels einer RecyclerView geht dieser Code „im Rauschen unter“.
Der Fakt, dass es deutlich einfacher ist UI Anteile mit Jetpack Compose umzusetzen legt nahe, dass es sich durchaus lohnen kann neue Features oder weitere Ausprägungen bestehender Listen als Composable umzusetzen.
Vor allem, wenn aufgrund der Komplexität oder dem Varianten-Reichtum der bestehenden Liste nicht mit einer vollständigen Migration in kurzer Zeit zu rechnen ist, bieten Jetpack Compose Interop APIs eine Möglichkeit für den sanften Umstieg.
Für noch mehr hilfreiche Tipps, wie du deine Bestands App zu Jetpack Compose migrieren kannst findest du hier weitere Beiträge.