Du betrachtest gerade Alle wichtigen Fakten zu View Compose Interop APIs – Lohnt sich das für meine App in 2024?

Alle wichtigen Fakten zu View Compose Interop APIs – Lohnt sich das für meine App in 2024?

Die überwiegende Mehrzahl von Android Apps besteht nach wie vor zu großen Teilen aus Views und XML Layouts. Der Wille zum Umstieg hin zum neuen UI Framework Jetpack Compose ist grundsätzlich vorhanden, doch halten die von Google angepriesenen Compose Interop APIs für Views wirklich was sie versprechen? Einen nahezu reibungslosen Umstieg von meiner App auf Jetpack Compose, bei dem ich erstmal alle meine wichtigsten alten „Android Legacy Views“ weiter nutzen kann?

Nun, das klingt wahrlich zu schön um wahr zu sein. Wir als von Natur aus misstrauische Software Engineers trauen dem Braten selbstverständlich nicht und prüfen die Compose Interop APIs, bzw. deren Anwendung, auf ihre Sinnhaftigkeit. Dabei betrachten wir typische Situationen, auf die wir immer wieder stoßen werden im Rahmen der Migration unserer „Legacy“ UI zu Jetpack Compose.

Compose Interop APIs für Android Legacy Views

Der häufigste Einsatzbereich für Compose Interop APIs ist die Wiederverwendung bereits existierender Android Views aus XML Layouts in einem Compose Layout mittels der „AndroidView“ Klasse:

@Composable
fun MediaPlayBar(
    cover: ImageBitmap,
    title: String,
    interpret: String,
    currentPlayTime: Int,
    totalPlayTime: Int,
    modifier: Modifier = Modifier
) {
    AndroidView(
        modifier = modifier.fillMaxWidth(),
        factory = { context ->
            // Reuses an existing View implementation
            MediaPlayBarView(context)
        }, update = { playBar ->
            // Connect @Composable parameters to View parameters
            playBar.cover = cover.asAndroidBitmap()
            playBar.title = title
            playBar.interpret = interpret
            playBar.currentPlayTime = currentPlayTime
            playBar.totalPlayTime = totalPlayTime
        }
    )
}

In aller Regel handelt es sich hierbei um sog. “Custom Views”, die wir selbst entwickelt haben, oder Views aus Libraries von Drittanbietern, die noch keine Compose Implementierungen bereitstellen. Beide haben gemeinsam, dass sie nicht Teil des Android Frameworks sind. Für Android Views des Frameworks gibt es bereits fertige Compose Varianten aus Googles Material Bibliothek, was deren Einbindung via Interop APIs überflüssig macht (Ausnahmen bestätigen hierbei die Regel).

Recycling bestehender Views für Compose und seine Grenzen

Diese Möglichkeiten des Recycling bestehender Views oder der Einsatz neuer Composable in Legacy Apps haben Grenzen. Diese Grenzen sind in der Regel nicht technischer Natur, mit den Compose Interop APIs ist gefühlt alles möglich.

Achte beim Recycling bestehender Views stets auf den "Kosten-Nutzen-Faktor"
Alle wichtigen Fakten zu View Compose Interop APIs - Lohnt sich das für meine App in 2024? 5

Die Grenzen liegen vielmehr im Bereich des “Kosten-Nutzen-Faktors”, oder anders ausgedrückt: “Lohnt sich der Aufwand dafür am Ende des Tages überhaupt?”. Nachfolgend einige Szenarien, in denen sich das Recycling bestehender Views mittels Interop APIs erfahrungsgemäß nicht lohnen.

Wiederverwenden einer View mit vielen Varianten in Compose

Viele Varianten innerhalb einer View, oder eines „Super Widgets“ führen in diesen immer zu großer Komplexität, die sich schwer warten lässt. Dieses „Super-Widget“ in all seinen Varianten in Compose versuchen abzubilden wäre nicht vorteilhaft, da wir:

  1. All diese möglichen Varianten als Funktions-Parameter abbilden müssen. Wir werden sehr schnell eine riesige Composable Funktion mit dutzenden Parametern schaffen, die am Ende niemand mehr durchblicken kann.
  2. Innerhalb unserer Compose-Wrapper Implementierung müssen wir aller Voraussicht nach viel an Mapping-Logik einbauen, um unsere Compose typischen Parameter auf die zu erwartenden View spezifischen Parameter zu konvertieren.
  3. Wir hätten neben unserer bereits sehr komplexen „Super-Widget“ Implementierung einen „Super-Adapter“ geschaffen, der diesem in Komplexität in nichts nachstehen dürfte.
@Composable
fun MediaControls(
    modifier: Modifier = Modifier,
    playIcon: ImageBitmap? = null,
    pauseIcon: ImageBitmap? = null,
    showPlayButton: Boolean = false,
    showPauseButton: Boolean = false,
    onPlayClick: () -> Unit,
    onPauseClick: () -> Unit,
    // And many more parameters ...
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            MediaControlsView(context)
        },
        update = { mediaControlsView ->
            mediaControlsView.playButton = playIcon?.asAndroidBitmap()
            mediaControlsView.pauseButton = pauseIcon?.asAndroidBitmap()
            mediaControlsView.showPlayButton = showPlayButton
            mediaControlsView.showPauseButton = showPauseButton
            mediaControlsView.setOnPlayClickListener = onPlayClick
            mediaControlsView.setOnPauseClickListener = onPauseClick
            // And many more parameters ...
        }
    )
}

Anstelle dessen sollten wir die Gelegenheit nutzen, um besagtes „Super-Widget“ in Compose neu zu implementieren und seine Varianten im Zuge dessen „aufzubrechen“.

Views in Composable abstrahieren die viele Styling-Attribute oder Varianten enthalten

Die Konvertierung von View-spezifischen Styling-Attributen zu Compose Styling Parametern kann schnell aus dem Ruder laufen:

  1. Styling-Attribute für Views sind nicht immer 1:1 kompatibel mit den typischen Compose Styling Parametern und Konzepten. Wir brauchen viel Mapping-Logik, um zum gleichen Ergebnis zu kommen.
  2. Es existieren bewusst Konzept-Brüche zwischen der Compose- und View-Styling Welt. So lassen sich zum Beispiel nötige Stylings für View „TextAppearance“ nur mit sehr großen Aufwand (und auch nicht vollständig) aus den Compose „TextStyles“ ableiten.
@Composable
fun MediaText(
    title: String,
    titleTextStyle: TextStyle,
    modifier: Modifier = Modifier
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            MediaTextView(context).apply { 
                titleTextView.setTextColor(titleTextStyle.color.toArgb())
                titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, titleTextStyle.fontSize.value)
                // And many many more necessary conversions from the TextStyle ...
            }
        },
        update = { mediaText ->
            mediaText.titleTextView.text = title
        }
    )
}

Die bessere Lösung ist es besagte View als ein Composable, oder eine Vielzahl von Composable, zu implementieren, um die nötigen Styling-Variationen abzubilden.

Reduziere Komplexität durch Compose

Oftmals brauchen wir den Overhead, der zum Beispiel durch das Einbinden einer Android Legacy View in Compose entsteht, gar nicht. Aus dem einfachen Grund, weil es in Compose oftmals bereits eine einfachere Lösung für das Problem gibt. Unser Ziel sollte immer folgendes sein: „Reduziere Komplexität durch Compose“ wo immer es nur geht!

Reduziere Komplexität durch Compose, verzichte auf Recycling von Views
Alle wichtigen Fakten zu View Compose Interop APIs - Lohnt sich das für meine App in 2024? 6

In etlichen Szenarien werden wir erleben, dass es signifikant umständlicher ist eine existierende View API nach Compose zu “mappen”, als sie in einem neuen Composable adäquat abzubilden.

Alte “Super-Widgets” loswerden

Android Legacy Views sind häufig über die Jahre immer weiter “gewachsen”, was ihren Funktionsumfang angeht. Dabei wird man oftmals auf unsere weiter oben erwähnten “Super-Widgets” treffen, die möglichst viele Funktionen und Features in einer View-Implementierung umsetzen. Das ist nicht ideal und nur mit sehr viel Aufwand testbar und dementsprechend schwierig wartbar. Die häufigsten UI Regressionen treten nicht zufällig in genau diesen “Super-Widgets” auf.

Compose löst diese Komplexität an vielen Stellen auf, alleine schon durch seine modulare Natur eines deklarativen Frameworks. Sonderfälle oder Variationen lassen sich leicht durch mehrere kleinere Composable abbilden. Dynamische Inhalte werden mit “Layout Slots” ermöglicht, ohne die berüchtigte “Adapter-Keule” auspacken zu müssen:

@Composable
fun MediaControls(
    modifier: Modifier = Modifier,
    isPlaying: Boolean = true,
    // Introducing layout slots for possible buttons
    playButton: @Composable RowScope.() -> Unit,
    pauseButton: @Composable RowScope.() -> Unit,
    // More buttons ...
) {
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        // Place buttons depending on surrounding UI logic
        if (isPlaying) {
            playButton()   
        } else {
            pauseButton()   
        }
    }
}

Die Verwendung dieses Composable ist deutlich modularer möglich, als mit unserem bereits bestehenden „Super-Widget“:

MediaControls(
        playButton = {
            Button(onClick = { /* Invoke "play" callback */ }) {
                Icon(
                    imageVector = Icons.Default.PlayArrow,
                    contentDescription = "play"
                )
            }
        },
        pauseButton = {
            Button(onClick = { /* Invoke "pause" callback */ }) {
                Icon(
                    imageVector = Icons.Default.Pause,
                    contentDescription = "pause"
                )
            }
        }
    )

Unsere resultierende Compose Lösung ist deutlich flexibler, den zusätzlichen Overhead den wir benötigen, um unser altes „Super-Widget“ weiter nutzen zu können sparen wir uns. Die dadurch gesparte Zeit können wir anderweitig besser investieren für die weitere Migration unserer App zu Jetpack Compose.

Fazit: Neu implementieren ist nicht immer schlecht

Also Software-Entwickler scheuen wir allzu oft den Aufwand eine bereits existierende Implementierung verwaisen zu lassen und diese nochmals zu bauen. Wir haben schließlich schon eine Menge Zeit und Geld für unsere Lösung investiert, diese wollen wir instinktiv nicht als “verschenkte Zeit” betrachtet sehen. Und das sollten wir auch nicht!

Unsere bisherige Lösung hat für das bisherige View und XML Framework gut funktioniert. Durch Jetpack Compose gelten nun aber mitunter andere “Spielregeln”, die wir respektieren müssen, wollen wir das volle Potential seiner Vorteile ausschöpfen.

Bevor wir also mit “Gewalt” versuchen unsere bestehenden komplexen und über Jahre gewachsenen Views in Compose zu recyceln, sollten wir vielmehr darüber nachdenken, ob eine Neu-Implementierung in Compose nicht mehr Sinn macht. Um Komplexität, aufwändige Wartbarkeit und umfangreiche Testbarkeit unserer UI Anteile in den Griff zu kriegen.

Alle wichtigen Fakten zu View Compose Interop APIs - Lohnt sich das für meine App in 2024?

Alles über Jetpack Compose & SW Engineering!

Abonniere meinen Newsletter!

Ich schicke dir keinen Spam! Lies meine Datenschutzhinweise für mehr Informationen.

Alles über Jetpack Compose & Software Engineering!

Abonniere meinen kostenlosen Newsletter und verpasse keinen neuen Artikel zu Jetpack Compose oder Software Engineering!

Ich schicke dir keinen Spam! Lies meine Datenschutzhinweise für mehr Informationen.

Give me some feedback, suggestions or just leave a Reply