Jetpack Compose basiert auf Kotlin. In einigen Fällen bietet Kotlin spezielle Idiome, die das Schreiben von gutem Compose-Code erleichtern. Wenn Sie in einer anderen Programmiersprache denken und diese Sprache mental in Kotlin übersetzen, verpassen Sie wahrscheinlich einige der Stärken von Compose und es fällt Ihnen möglicherweise schwer, idiomatisch geschriebenen Kotlin-Code zu verstehen. Wenn Sie sich mit dem Stil von Kotlin vertraut machen, können Sie diese Probleme vermeiden.
Standardargumente
Wenn Sie eine Kotlin-Funktion schreiben, können Sie Standardwerte für Funktionsargumente angeben, die verwendet werden, wenn der Aufrufer diese Werte nicht explizit übergibt. Diese Funktion macht überladene Funktionen überflüssig.
Angenommen, Sie möchten eine Funktion schreiben, die ein Quadrat zeichnet. Diese Funktion hat möglicherweise einen einzelnen erforderlichen Parameter, sideLength, der die Länge jeder Seite angibt. Sie kann mehrere optionale Parameter wie thickness und edgeColor haben. Wenn der Aufrufer diese nicht angibt, verwendet die Funktion Standardwerte. In anderen Sprachen müssen Sie möglicherweise mehrere Funktionen schreiben:
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
In Kotlin können Sie eine einzelne Funktion schreiben und die Standardwerte für die Argumente angeben:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
So müssen Sie nicht mehrere redundante Funktionen schreiben und Ihr Code ist viel übersichtlicher. Wenn der Aufrufer keinen Wert für ein Argument angibt, bedeutet das, dass er den Standardwert verwenden möchte. Außerdem ist es durch die benannten Parameter viel einfacher zu erkennen, was passiert. Wenn Sie sich den Code ansehen und einen Funktionsaufruf wie diesen sehen, wissen Sie möglicherweise nicht, was die Parameter bedeuten, ohne den drawSquare()
-Code zu prüfen:
drawSquare(30, 5, Color.Red);
Im Gegensatz dazu ist dieser Code selbstdokumentierend:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
Die meisten Compose-Bibliotheken verwenden Standardargumente. Es empfiehlt sich, dies auch für die von Ihnen geschriebenen zusammensetzbaren Funktionen zu tun. So können Sie Ihre Composables anpassen und gleichzeitig das Standardverhalten einfach aufrufen. So können Sie beispielsweise ein einfaches Textelement erstellen:
Text(text = "Hello, Android!")
Dieser Code hat dieselbe Wirkung wie der folgende, viel ausführlichere Code, in dem mehr Parameter von Text
explizit festgelegt werden:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
Das erste Code-Snippet ist nicht nur viel einfacher und leichter zu lesen, sondern auch selbstdokumentierend. Wenn Sie nur den Parameter text
angeben, dokumentieren Sie, dass Sie für alle anderen Parameter die Standardwerte verwenden möchten. Im zweiten Snippet werden die Werte für diese anderen Parameter explizit festgelegt, auch wenn die von Ihnen festgelegten Werte die Standardwerte für die Funktion sind.
Funktionen höherer Ordnung und Lambda-Ausdrücke
Kotlin unterstützt Funktionen höherer Ordnung, also Funktionen, die andere Funktionen als Parameter empfangen. Compose baut auf diesem Ansatz auf. Die zusammensetzbare Funktion Button
bietet beispielsweise einen Lambda-Parameter onClick
. Der Wert dieses Parameters ist eine Funktion, die von der Schaltfläche aufgerufen wird, wenn der Nutzer darauf klickt:
Button( // ... onClick = myClickFunction ) // ...
Funktionen höherer Ordnung passen gut zu Lambda-Ausdrücken, die zu einer Funktion ausgewertet werden. Wenn Sie die Funktion nur einmal benötigen, müssen Sie sie nicht an anderer Stelle definieren, um sie an die Funktion höherer Ordnung zu übergeben. Stattdessen können Sie die Funktion direkt mit einem Lambda-Ausdruck definieren. Im vorherigen Beispiel wird davon ausgegangen, dass myClickFunction()
an anderer Stelle definiert ist. Wenn Sie diese Funktion jedoch nur hier verwenden, ist es einfacher, die Funktion inline mit einem Lambda-Ausdruck zu definieren:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Nachgestellte Lambdas
Kotlin bietet eine spezielle Syntax zum Aufrufen von Funktionen höherer Ordnung, deren letzter Parameter ein Lambda ist. Wenn Sie einen Lambda-Ausdruck als Parameter übergeben möchten, können Sie die Syntax für nachgestellte Lambdas verwenden. Anstatt den Lambda-Ausdruck in die Klammern zu setzen, setzen Sie ihn danach. Das ist eine häufige Situation in Compose. Sie müssen also wissen, wie der Code aussieht.
Der letzte Parameter für alle Layouts, z. B. für die zusammensetzbare Funktion Column()
, ist content
, eine Funktion, die die untergeordneten UI-Elemente ausgibt. Angenommen, Sie möchten eine Spalte mit drei Textelementen erstellen und müssen einige Formatierungen anwenden. Dieser Code würde funktionieren, ist aber sehr umständlich:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
Da der Parameter content
der letzte in der Funktionssignatur ist und wir seinen Wert als Lambda-Ausdruck übergeben, können wir ihn aus den Klammern herausziehen:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
Die beiden Beispiele haben genau dieselbe Bedeutung. Die geschweiften Klammern definieren den Lambda-Ausdruck, der an den Parameter content
übergeben wird.
Wenn der einzige Parameter, den Sie übergeben, das nachgestellte Lambda ist, d. h., wenn der letzte Parameter ein Lambda ist und Sie keine anderen Parameter übergeben, können Sie die Klammern ganz weglassen. Angenommen, Sie müssen keinen Modifier an Column
übergeben. Sie könnten den Code so schreiben:
Column { Text("Some text") Text("Some more text") Text("Last text") }
Diese Syntax ist in Compose recht häufig, insbesondere für Layoutelemente wie Column
. Der letzte Parameter ist ein Lambda-Ausdruck, der die untergeordneten Elemente des Elements definiert. Diese untergeordneten Elemente werden in geschweiften Klammern nach dem Funktionsaufruf angegeben.
Bereiche und Empfänger
Einige Methoden und Eigenschaften sind nur in einem bestimmten Bereich verfügbar. Mit dem eingeschränkten Umfang können Sie Funktionen dort anbieten, wo sie benötigt werden, und vermeiden, dass sie versehentlich an Stellen verwendet werden, an denen sie nicht angebracht sind.
Sehen Sie sich ein Beispiel an, das in Compose verwendet wird. Wenn Sie die zusammensetzbare Funktion Row
aufrufen, wird Ihr Inhalts-Lambda automatisch in einem RowScope
aufgerufen.
So kann Row
Funktionen bereitstellen, die nur innerhalb eines Row
gültig sind.
Im folgenden Beispiel sehen Sie, wie Row
einen zeilenspezifischen Wert für den Modifikator align
bereitgestellt hat:
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
Einige APIs akzeptieren Lambdas, die im Empfängerbereich aufgerufen werden. Diese Lambdas haben Zugriff auf Eigenschaften und Funktionen, die an anderer Stelle definiert sind, basierend auf der Parameterdeklaration:
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
Weitere Informationen finden Sie in der Kotlin-Dokumentation unter Funktionsliterale mit Empfänger.
Delegierte Properties
Kotlin unterstützt delegierte Eigenschaften.
Diese Properties werden so aufgerufen, als wären sie Felder, aber ihr Wert wird dynamisch durch Auswerten eines Ausdrucks bestimmt. Sie erkennen diese Eigenschaften an der Verwendung der by
-Syntax:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
Anderer Code kann mit folgendem Code auf die Property zugreifen:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
Wenn println()
ausgeführt wird, wird nameGetterFunction()
aufgerufen, um den Wert des Strings zurückzugeben.
Diese delegierten Eigenschaften sind besonders nützlich, wenn Sie mit zustandsbasierten Eigenschaften arbeiten:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Datenklassen dekonstruieren
Wenn Sie eine Datenklasse definieren, können Sie mit einer Destrukturierungsdeklaration ganz einfach auf die Daten zugreifen. Angenommen, Sie definieren eine Person
-Klasse:
data class Person(val name: String, val age: Int)
Wenn Sie ein Objekt dieses Typs haben, können Sie mit folgendem Code auf seine Werte zugreifen:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
Dieser Code wird häufig in Compose-Funktionen verwendet:
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
Datenklassen bieten viele weitere nützliche Funktionen. Wenn Sie beispielsweise eine Datenklasse definieren, definiert der Compiler automatisch nützliche Funktionen wie equals()
und copy()
. Weitere Informationen finden Sie in der Dokumentation zu Datenklassen.
Singleton-Objekte
In Kotlin ist es ganz einfach, Singletons zu deklarieren, also Klassen, von denen es immer nur eine Instanz gibt. Diese Singletons werden mit dem object
-Schlüsselwort deklariert.
Compose verwendet solche Objekte häufig. MaterialTheme
ist beispielsweise als Singleton-Objekt definiert. Die Attribute MaterialTheme.colors
, shapes
und typography
enthalten alle die Werte für das aktuelle Design.
Typsichere Builder und DSLs
Mit Kotlin lassen sich domänenspezifische Sprachen (DSLs) mit typsicheren Buildern erstellen. Mit DSLs lassen sich komplexe hierarchische Datenstrukturen auf wartungsfreundlichere und lesbarere Weise erstellen.
Jetpack Compose verwendet DSLs für einige APIs wie LazyRow
und LazyColumn
.
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
Kotlin garantiert typsichere Builder mithilfe von Funktionsliteralen mit Empfänger.
Nehmen wir als Beispiel die zusammensetzbare Funktion Canvas
. Sie verwendet als Parameter eine Funktion mit DrawScope
als Empfänger, onDraw: DrawScope.() -> Unit
. Dadurch kann der Codeblock Memberfunktionen aufrufen, die in DrawScope
definiert sind.
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
Weitere Informationen zu typsicheren Buildern und DSLs finden Sie in der Kotlin-Dokumentation.
Kotlin-Koroutinen
Coroutinen bieten Unterstützung für asynchrone Programmierung auf Sprachebene in Kotlin. Coroutinen können die Ausführung anhalten, ohne Threads zu blockieren. Eine reaktionsfähige Benutzeroberfläche ist von Natur aus asynchron. Jetpack Compose löst dieses Problem, indem es Coroutinen auf API-Ebene verwendet, anstatt Callbacks zu nutzen.
Jetpack Compose bietet APIs, mit denen die Verwendung von Coroutinen in der UI-Schicht sicher ist.
Die Funktion rememberCoroutineScope
gibt ein CoroutineScope
zurück, mit dem Sie Coroutinen in Ereignishandlern erstellen und Compose-Suspend-APIs aufrufen können. ScrollState
-Beispiel mit der animateScrollTo
-API:
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
Bei Coroutinen wird der Codeblock standardmäßig sequenziell ausgeführt. Eine laufende Coroutine, die eine Suspend-Funktion aufruft, hält ihre Ausführung an, bis die Suspend-Funktion zurückkehrt. Das gilt auch dann, wenn die Ausführung durch die Suspend-Funktion auf einen anderen CoroutineDispatcher
verschoben wird. Im vorherigen Beispiel wird loadData
erst ausgeführt, wenn die suspend-Funktion animateScrollTo
zurückgegeben wird.
Um Code gleichzeitig auszuführen, müssen neue Coroutinen erstellt werden. Im obigen Beispiel sind zwei Coroutinen erforderlich, um das Scrollen zum oberen Bildschirmrand und das Laden von Daten aus viewModel
zu parallelisieren.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
Durch Coroutinen wird es einfacher, asynchrone APIs zu kombinieren. Im folgenden Beispiel kombinieren wir den Modifikator pointerInput
mit den Animations-APIs, um die Position eines Elements zu animieren, wenn der Nutzer auf den Bildschirm tippt.
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen val offset = awaitPointerEventScope { awaitFirstDown().position } // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
Weitere Informationen zu Coroutinen finden Sie im Leitfaden Kotlin-Coroutinen in Android.
Empfehlungen für dich
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Material-Komponenten und ‑Layouts
- Nebeneffekte in Compose
- Grundlagen des Compose-Layouts