Beliebte Suchanfragen
//

Jetpack Compose: So gestaltest du deklarativ User Interfaces für Android-Apps

4.5.2023 | 5 Minuten Lesezeit

Anfang 2023 hatte ich ein paar kleine Ideen für spaßige Apps. Daher fing ich an, sie mit Kotlin und Android Studio umzusetzen. Leider fiel es mir von Anfang an schwer, mich in die XML-Dateien hineinzudenken und bei der Navigation ging es endgültig durcheinander. Ich wollte nicht umständlich an verschiedenen Stellen Änderungen durchführen, um sauber zwischen Fragmenten und Screens zu wechseln. Dann stieß ich auf Jetpack Compose.

Jetpack Compose ist Androids UI-Toolkit für die native, deklarative Entwicklung. Jetpack Compose wurde 2019 vorgestellt und erleichtert es mir, dynamische Benutzeroberflächen zu erstellen. Es basiert auf Kotlin und nutzt das reaktive Programmiermodell, ähnlich zu React. UI-Komponenten werden also automatisch aktualisiert, wenn sich der Anwendungsstatus oder die Benutzereingaben verändern. Dies erleichtert mir die Erstellung reaktionsfähiger Anwendungen für ein reibungsloses Benutzererlebnis. Der Hauptbaustein sind dabei die Composable. Es ist relevant, wo und wie ein Composable aufgerufen wird, da je nach call site und Parametern ein Composable unterschiedlich wiederverwendet werden kann.

Wie sieht so ein Composable nun aus? In einer meiner Apps brauche ich einen Button, der bei jedem Klick einen Counter hochzählt. Je nach Kontext will ich auch unterschiedliche Icons auf dem Button darstellen und übergebe diese daher mit:

1@Composable  
2fun IncrementButton(icon: ImageVector, onClick: () -> Unit) {  
3    Button(  
4        onClick = onClick, modifier = Modifier  
5            .padding(5.dp)  
6            .fillMaxWidth()  
7    ) {  
8        Text("+1")  
9        Icon(icon, contentDescription = null)  
10    }  
11}  
12  
13@Preview(name = "Light Mode")  
14@Preview(  
15    uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode"  
16)  
17@Composable  
18fun IncrementButtonPreview() {  
19    EcoIdlerTheme() {  
20        val width = LocalConfiguration.current.screenWidthDp.dp  
21        Row(  
22            modifier = Modifier  
23                .padding(top = 5.dp)  
24                .padding(end = 12.dp)  
25                .fillMaxWidth()  
26        ) {  
27            Column(modifier = Modifier.width(width / 3)) {  
28  
29                IncrementButton(icon = Icons.Default.Home, onClick = {})  
30            }  
31            Column(modifier = Modifier.fillMaxWidth()) {  
32                IncrementButton(icon = Icons.Default.Info, onClick = {})  
33                IncrementButton(icon = Icons.Default.AccountBox, onClick = {})  
34            }  
35        }    
36    }
37}

Wieso zwei Funktionen? Jetpack Compose bringt konfigurierbare Preview Decorators mit, anhand derer ich direkt in Android Studio sehen kann, wie das UI-Element gerendert aussieht. In Hell ist das einmal so:

Ich kann ein und dasselbe UI-Element in der IDE mit verschiedenen Einstellungen rendern, bspw. mit verschiedenen Font-Größen oder hier Themes. Die dafür verwendeten Decorator kann ich übrigens selbst wieder wrappen und damit detaillierte Previews mit einheitlichen Einstellungen umsetzen.

Was passiert genau im Codebeispiel? Der Composable-Decorator hebt das Composable vor. Innerhalb des IncrementButton definiere ich deklarativ meinen Button inkl. der Elemente darin, d. h. die Composable Text und Icon haben eine andere call site. Paddings oder andere Styling-Direktiven übergebe ich als Argument an Modifier. Hier erstelle ich jeweils einen neuen Modifier, kann den aber auch durchreichen und damit Styling weitergeben.

Die onClick-Methode deutet auch schon einen weiteren Vorteil von Jetpack Compose an: Es baut auf dem „Model-View-ViewModel”-Ansatz auf. Composables nehmen State und emittieren Events. Alle Veränderungen des – primitiven – State führen zu einer Aktualisierung des Composables.

Im Preview nutze ich Row und Column, die entsprechenden Composables zur horizontalen oder vertikalen Anordung von Elementen. Um etwas Variation in dieses einfache Composable aus drei Buttons zu bringen, mache ich sie unterschiedlich breit. Die Bildschirmbreite frage ich über folgenden Code ab:

1val width = LocalConfiguration.current.screenWidthDp.dp

Der Modifier fillMaxWidth() passt das Composable auf die ganze Breite an. Bereits an diesem Beispiel zeigt sich, wie lesbar Composables mit Jetpack sind. Ich schreibe keine XML-Dateien oder ziehe Verknüpfungen umher. Und ich muss nirgendwo das Device konfigurieren, auf dem ich die UI-Elemente darstelle.

Wie sieht es nun mit Listendarstellungen aus?

1class Names(var name: String, var surname: String)  
2  
3@Composable  
4fun NameList(names: MutableList<Names>) {  
5    LazyColumn {  
6        items(names.size) {  
7            Text(text = "Hallo, ich bin ${names[it].name} ${names[it].surname}.")  
8        }  
9    }}  
10  
11@Preview(name = "Light Mode")  
12@Composable  
13fun NameListPreview() {  
14    Surface {  
15        Column {  
16            val names = mutableListOf(Names("Peter", "Fuchs"), Names("Johann", "Tepler"))  
17            NameList(names)  
18        }  
19    }  
20}

Mit LazyColumn werden nur Einträge gerendert, die gerade sichtbar sind. Alternativ wäre auch eine Column mit forEach-Loop möglich. Aufgrund der LazyColumn muss ich leider items benutzen, um durch die Einträge zu loopen. Trotzdem wird der Code kürzer und übersichtlicher. Er lässt sich leichter refactoren.

Ändere ich jetzt einen Eintrag in dieser Liste von Namen, so wird der jeweilige geänderte Text neu gerendert. Schwieriger wird es, wenn Composables komplexere Typen, wie beispielsweise Listen von Strings übergeben bekommen. Die Reaktion auf Veränderungen findet dann meist nicht statt, v. a. wenn es sich um mutableList handelt. Alternativ bittet Jetpack Compose hier den Typ SnapshotStateList an, um auch auf Veränderungen in mutableList reaktiv zu reagieren. Allerdings funktioniert das auch nicht immer, weswegen manche Lösungen eine Art Trigger-Variable nutzen, die verändert wird, wenn irgendwas neu gerendert werden soll. Darüber gibt es aber durchaus noch Diskussionen.

Und das ist schon meine größte Hürde mit Jetpack Compose: Es ist relativ neu und entwickelt sich rasant. Dadurch ist die Dokumentation schon teilweise veraltet und es haben sich noch keine klaren Best Practices herausgebildet. Noch gibt es für viele Lösungen mehrere Wege. Zwar ist vieles darauf optimiert weniger Boilerplate-Code zu schreiben und mit Decorators sinnvoll Funktionalität anzuwenden, aber wann genau bspw. SnapshotStateList oder MutableList angewendet werden muss, wird noch heiß diskutiert. Oder auch gar nicht, wie manche doch recht leeren Stackoverflow-Diskussionen mir gezeigt haben. Von daher gilt hier noch mehr als sonst: aufpassen, was wo vorgeschlagen wird und ordentlich testen, was ihr baut.

Dafür könnte die Navigation kaum leichter sein. Mittels des integrierten Navcontroller kann ich deklarativ Routen abgehen:

1val navController = rememberNavController()  
2navController.navigate("Intro")

Bei größeren Projekten ist es sinnvoll auch die Navigation zentral abzuhalten und die Routen über eine sealed class abzubilden. Über entsprechende Signale von den Composables lässt sich das aber gut handhaben.

Insgesamt war Jetpack Compose für mich als Android-App-Neuling sehr interessant. Vieles fühlt sich intuitiv an und ich kann mich auf das Wesentliche konzentrieren. Ich muss nicht in mehreren XML-Dateien herumwursteln, um Navigation und ähnliches einzurichten. Die deklarative Erstellung der UI in Kombination mit den Previews in der IDE ist sehr komfortabel. CSS musste ich nirgendwo nutzen oder schreiben und für mich als sporadischer-Frontend-Entwickler ist das ein großes Plus.

Und übrigens: ChatGPT hilft bei Jetpack Compose kaum – hier muss also wirklich Menschenarbeit angelegt werden.

Beitrag teilen

Gefällt mir

1

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.