Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
Vorwort
Teil I Grundlagen
1 Android – eine offene, mobile Plattform
2 Hallo Android!
3 Von der Idee zur Veröffentlichung
Teil II Elementare Anwendungsbausteine
4 Wichtige Grundbausteine von Apps
5 Benutzeroberflächen
6 Multitasking
Teil III Gerätefunktionen nutzen
7 Telefonieren und surfen
8 Sensoren, GPS und Bluetooth
Teil IV Dateien und Datenbanken
9 Dateien lesen, schreiben und drucken
10 Datenbanken
Teil V Multimedia und Produktivität
11 Multimedia
12 Kontakte und Organizer
A Einführung in Kotlin
B Jetpack Compose
C Häufig benötigte Codebausteine
D Literaturverzeichnis
E Die Begleitmaterialien
Stichwortverzeichnis

Ihre Meinung?
Spacer
<< zurück
Android 11 von Thomas Künneth
Das Praxisbuch für App-Entwickler
Buch: Android 11

Android 11
Pfeil B Jetpack Compose
Pfeil B.1 Deklaratives Programmiermodell
Pfeil B.1.1 Imperative Ansätze
Pfeil B.1.2 Besser deklarativ
Pfeil B.2 Zustand und Ereignisse
Pfeil B.2.1 remember {}
Pfeil B.2.2 Auf Benutzereingaben reagieren
Pfeil B.2.3 Voransichten von Composables anzeigen
Pfeil B.3 Integration in Projekte
Pfeil B.3.1 Nur ab Kotlin 1.4
Pfeil B.3.2 Modulspezifische »build.gradle«-Datei
Pfeil B.4 Zusammenfassung
 
Zum Seitenanfang

B    Jetpack Compose Zur vorigen ÜberschriftZur nächsten Überschrift

Mit Jetpack Compose wird sich der Bau von Android-UIs radikal verändern. Noch ist die Bibliothek nicht fertig. Dieser Anhang zeigt Ihnen den aktuellen Stand und gibt eine Einführung in wichtige Konzepte.

Jetpack Compose ist ein modernes Framework für den Bau nativer Android-Benutzeroberflächen. Die Bibliothek basiert auf einem deklarativen Programmiermodell: Sie beschreiben nur noch, wie die Oberfläche aktuell aussehen soll. Das Framework kümmert sich um alle notwendigen Anpassungen.

 
Zum Seitenanfang

B.1    Deklaratives Programmiermodell Zur vorigen ÜberschriftZur nächsten Überschrift

Die Idee, Oberflächen zu beschreiben, ist nicht neu. Das Auslagern in Layoutdateien und die damit einhergehende Trennung vom Code ist ja eine Beschreibung. Hinter deklarativen Programmiermodellen steckt aber viel mehr. Damit Sie die Unterschiede nachvollziehen können, möchte ich mit Ihnen zunächst das bisherige Vorgehen reflektieren. Natürlich beziehe ich mich dabei auf Android. Sie sollten aber im Hinterkopf behalten, dass praktisch alle (noch) aktuellen UI-Frameworks so ähnlich funktionieren.

 
Zum Seitenanfang

B.1.1    Imperative Ansätze Zur vorigen ÜberschriftZur nächsten Überschrift

Android fasst Bedienelemente zur Laufzeit zu baumartigen Strukturen zusammen. Deren Knoten sind Objekte. Die Wurzel hängen Sie beispielsweise mit setContentView() in eine Activity ein oder liefern sie in Fragmenten bei onCreateView() zurück. Kinder sind entweder einfache Elemente wie Button, TextView oder ImageView oder aber ViewGroups. Diese können – wie die Wurzel – wiederum normale Bedienelemente sowie weitere ViewGroups enthalten. Jeder Knoten speichert elementare Werte wie Position, Breite und Höhe. ViewGroups kümmern sich um das Layout, sie berechnen also Position und Größe ihrer Kinder. Weitere Werte der Knoten sind oft elementspezifisch, beispielsweise der Text eines Buttons, dessen Farbe und seine Schriftart.

Um eine Benutzeroberfläche zum Leben zu erwecken, muss der Entwickler das Objektgeflecht zunächst erstellen und dann situationsgerecht manipulieren. Die meisten Attribute der UI-Komponenten können deshalb gelesen und geschrieben werden. Unter Android wird das Layout in einer XML-Datei definiert und in einer Activity oder einem Fragment entfaltet und angezeigt. UI-Frameworks anderer Plattformen gehen hier ähnlich vor, trennen also Beschreibung und Manipulation. Wirklich nötig ist das aber nicht – alle Frameworks (auch Android) können Oberflächen rein programmatisch erstellen und manipulieren. Aber was meine ich eigentlich mit manipulieren?

Da die Benutzeroberfläche zur Laufzeit ein Objektgeflecht ist, müssen Sie jede gewünschte Änderung haarklein vorgeben. Soll ein Teil des Baumes nicht zu sehen sein, müssen Sie diesen mit visibiliy = View.GONE ausblenden. Darf ein Button nicht angeklickt werden, deaktivieren Sie ihn mit isEnabled = false. Und soll je nach Auswahl eines RadioButtons ein anderer Teilbaum angezeigt werden, müssen Sie entweder alle Teilbäume in der Oberfläche vorsehen und entsprechend ein- und ausblenden (dann ist der Komponentenbaum unnötig groß) oder dynamisch nachladen.

Natürlich funktioniert das alles in kleinen (und grundsätzlich auch in großen) Projekten problemlos. Aber je mehr Daten Sie in Ihrer App halten, umso aufwendiger wird es für Sie, den Überblick zu behalten. Welche Auswirkungen hat die Änderung von Daten auf die Oberfläche? Wann ist was zu sehen? Listing B.1 zeigt einen Auszug aus meinem Beispiel Hallo Android (Kapitel 2, »Hallo Android!«). message (eine TextView) wird mit »Guten Tag. Schön, dass Sie mich ...« vorbelegt. Die Schaltfläche nextFinish hat zunächst den Text »Weiter«. Nach dem Anklicken des Buttons lautet sein Text »Fertig«, message zeigt eine Grußfloskel, und ein Eingabefeld (input) wird unsichtbar. Ein weiterer Buttonklick beendet die Activity. Konzeptionell hängt die (kleine) Oberfläche von der Variable firstClick ab. Das ist aber in einer unscheinbaren if-Anweisung versteckt.

message.setText(R.string.welcome)
nextFinish.setText(R.string.next)
nextFinish.setOnClickListener(fun(_: View) {
if (firstClick) {
message.text = getString(
R.string.hello,
input.text
)
input.visibility = View.INVISIBLE
nextFinish.setText(R.string.finish)
firstClick = false
} else {
finish()
}
})

Listing B.1    Auszug aus dem Projekt »Hallo Android«

 
Zum Seitenanfang

B.1.2    Besser deklarativ Zur vorigen ÜberschriftZur nächsten Überschrift

Deklarative Programmiermodelle stellen nicht mehr das Was?, sondern das Wie? in den Mittelpunkt. Statt die Oberfläche auf Basis von Zustandsänderungen zu modifizieren, kann man sie nämlich auch situationsabhängig beschreiben. Für das Hallo Android-Beispiel bedeutet das:

  • Die Oberfläche ist von genau einem Wert abhängig: der Variablen firstClick.

  • Der Button zeigt entweder Weiter oder Fertig an.

  • Wurde er noch nicht angeklickt, erscheint über ihm eine Willkommensmeldung.

  • Wurde er mindestens einmal angeklickt, erscheint stattdessen die Grußfloskel.

Bei deklarativ erzeugten Benutzeroberflächen spielen nur die Bedienelemente eine Rolle, die aktuell dargestellt werden sollen. Ob sie früher schon vorhanden waren und deshalb aktualisiert oder neu hinzukommen und deshalb in den Komponentenbaum eingefügt werden müssten, kann dem Entwickler egal sein. Auch, ob Elemente wegfallen. Ihn interessiert nur die augenblickliche Situation. Deshalb beschreibt er auch nur diese. Wie, zeige ich Ihnen anhand der App Hallo Android Compose (Abbildung B.1).

Die App »Hallo Android Compose«

Abbildung B.1    Die App »Hallo Android Compose«

[»]  Hinweis

Jetpack Compose muss sehr genau Buch führen, welche Änderungen nötig sind. Ein vollständiges Neuzeichnen würde zu deutlichem Flackern führen und unnötig Rechenzeit kosten. Aber um solche Interna müssen Sie sich zum Glück nicht kümmern.

Zum Zeitpunkt der Drucklegung ist für die Verwendung von Jetpack Compose eine Vorschauversion von Android Studio im Kanal Canary nötig. Diese enthält aktualisierte Projektvorlagen und eine Voransicht für Compose-Benutzeroberflächen.

Die Hauptklasse HalloAndroidComposeActivity ist in Listing B.2 zu sehen. Schon beim ersten Überfliegen fallen mehrere Funktionen auf, die mit @Composable annotiert sind. Damit definieren Sie die Bausteine, aus denen die Benutzeroberfläche Ihrer App zusammengesetzt wird.

Composable functions

Mein Beispiel besteht demnach aus ContentView, FirstPage, SecondPage, GreetingText und MyButton. Die Funktion DefaultPreview nimmt eine Sonderstellung ein. Sie wird für das Anzeigen der Voransicht benötigt. Deshalb auch @Preview. Composable functions beginnen gemäß Googles Kotlin Style Guide im Unterschied zu normalen Funktionen mit einem Großbuchstaben.[ 22 ](https://developer.android.com/kotlin/style-guide#function_names) Verwenden Sie Substantive, als wären die Funktionen Datentypen. Wie die Benutzeroberfläche zusammengesetzt wird, können Sie schön nachvollziehen, indem Sie der Aufrufkette folgen. Sie beginnt in onCreate(). setContent() erweitert die Klasse ComponentActivity. Die ihr übergebene Funktion (ContentView()) liefert die Wurzel, analog zu setContentView() in der alten Welt.

In meinem Beispiel ist die Wurzel das Composable Column. Es nimmt ein oder mehrere Kinder auf und ordnet sie untereinander an. Mit horizontalAlignment = Alignment.CenterHorizontally werden sie zentriert. fillMaxWidth() sorgt dafür, dass die Column die komplette Breite einnimmt. padding(16.dp) gibt ihr einen Rahmen, der an allen vier Seiten 16 geräteunabhängige Pixel groß ist. Die Kinder werden durch firstPage.value festgelegt. Was es damit auf sich hat, sehen wir etwas später. Lassen Sie uns vorher noch einen Blick auf FirstPage() und SecondPage() werfen.

package com.thomaskuenneth.androidbuch.halloandroidcompose

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.*
import androidx.ui.tooling.preview.Preview

private val HEIGHT = 96.dp
class HalloAndroidComposeActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ContentView { finish() }
}
}
}

@Composable
fun ContentView(finish: () -> Unit) {
val firstPage = remember { mutableStateOf(true) }
val name = remember { mutableStateOf("") }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(16.dp)
) {
if (firstPage.value) {
FirstPage(HEIGHT, name.value) { currentName: String ->
firstPage.value = false
name.value = currentName
}
} else {
SecondPage(HEIGHT, name.value) { finish() }
}
}
}

@Composable
fun FirstPage(height: Dp, initial: String, onClick: (name: String)
-> Unit) {
val name = remember { mutableStateOf(initial) }
val enabled = remember { mutableStateOf(false) }
GreetingText(
stringResource(R.string.welcome)
)
Box(
alignment = Alignment.TopCenter,
modifier = Modifier.preferredHeight(height)
) {
OutlinedTextField(
value = name.value,
placeholder = { Text(stringResource(R.string.firstname_surname)) },
onValueChange = {
name.value = it
enabled.value = name.value.isNotEmpty()
},
imeAction = ImeAction.Next,
onImeActionPerformed = { _, _ ->
if (enabled.value)
onClick(name.value)
},
modifier = Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.your_name)) }
)
}
MyButton(stringResource(R.string.next), enabled.value) {
onClick(name.value)
}
}

@Composable
fun SecondPage(height: Dp, name: String, onClick: () -> Unit) {
GreetingText(
stringResource(R.string.hallo, name)
)
Spacer(modifier = Modifier.preferredHeight(height))
MyButton(stringResource(R.string.done), true) { onClick() }
}

@Composable
fun GreetingText(text: String) {
Text(
text = text,
textAlign = TextAlign.Start,
modifier = Modifier.preferredHeight(48.dp) )
}

@Composable
fun MyButton(text: String, enabled: Boolean, onClick: () -> Unit) {
Button(
onClick = onClick,
enabled = enabled
) {
Text(text = text)
}
}

@Preview
@Composable
fun DefaultPreview() {
MaterialTheme {
ContentView {}
}
}

Listing B.2    Die Klasse »HalloAndroidComposeActivity«

FirstPage() fügt unserer Benutzeroberfläche mehrere Composables hinzu. GreetingText() und MyButton() habe ich implementiert. Box() und OutlinedTextField() gehören zu Jetpack Compose. Das Textfeld wird in eine Box gepackt, weil unterhalb des Textfeldes ein definierter Platz frei bleiben soll. Andernfalls würde nach dem ersten Anklicken die Schaltfläche »springen«, also ihre vertikale Position verändern. Das lässt sich zwar auch auf anderem Wege realisieren (beispielsweise könnten Sie OutlinedTextField() einen zusätzlichen Modifier übergeben), aber so haben Sie gleich Box() kennengelernt.

Ist Ihnen aufgefallen, dass placeholder und label keine Strings sind, sondern ebenfalls Composables (Text())? Die Philosophie von Jetpack Compose ist, die Oberfläche vollständig mit Composables umzusetzen. So können sich der Platzhalter und das Label individuell der aktuellen Situation anpassen. SecondPage() fügt der Column() einen GreetingText(), MyButton() und einen Spacer() hinzu. Er sorgt für freien Platz. Der Button bleibt so an seiner Position. Modifier steuern, wie ein Composable innerhalb der UI angezeigt wird. preferredHeight() legt die gewünschte Höhe fest, fillMaxWidth() sorgt für eine maximale Breite.

Jetpack Compose beinhaltet zahlreiche Bedienelemente: Ein paar haben Sie in meinem Beispiel gesehen. Sie sollten Ihre Oberfläche in möglichst viele sinnvolle eigene Composables zerlegen. Bitte beachten Sie hierbei, dass Ihre Funktionen möglicherweise sehr häufig aufgerufen werden. Der Code darf deshalb keine rechenintensiven Operationen durchführen. Werte übergeben Sie als Parameter von außen. Das gilt üblicherweise auch für Aktionen, beispielsweise nach dem Anklicken eines Buttons. Nur wenn ein Ereignis Auswirkungen auf das Composable selbst hat, wird es dort verarbeitet. FirstPage() beispielsweise enthält ein Eingabefeld und eine Schaltfläche. Diese kann nur angeklickt werden, wenn mindestens ein Zeichen eingetippt wurde. Deshalb findet die Reaktion auf Tastendrücke (onValueChange) direkt in FirstPage() statt. Hierzu wird der Status mit enabled.value = name.value.isNotEmpty() ermittelt und an MyButton() übergeben.

Hier sehen Sie sehr schön den größten Unterschied zwischen deklarativen und imperativen Programmiermodellen. Es wird aus Sicht des Programmierers nicht der Status einer Instanz geändert, sondern das Composable wird mit den aktuell gewünschten Werten aufgerufen. Wie das zur Laufzeit abgebildet wird, ist für die Entwicklung unerheblich.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
Zur Rheinwerk-Konferenz für Kotlin
 Buchempfehlungen
Zum Rheinwerk-Shop: Kotlin

Kotlin


Zum Rheinwerk-Shop: Praxisbuch Usability und UX

Praxisbuch Usability und UX


Zum Rheinwerk-Shop: Flutter und Dart

Flutter und Dart


Zum Rheinwerk-Shop: App-Design

App-Design


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2023
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern