Jetpack Compose basiert auf Kotlin. In einigen Fällen bietet Kotlin spezielle Idiome, die das Schreiben von gutem Compose-Code vereinfachen. Wenn Sie in einer anderen Programmiersprache denken und diese Sprache gedanklich in Kotlin übersetzen, entgehen Ihnen wahrscheinlich einige der Stärken von Compose und es kann schwierig sein, idiomatisch geschriebenen Kotlin-Code zu verstehen. Wenn Sie sich mit dem Stil von Kotlin vertraut machen, können Sie diese Schwierigkeiten 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 reduziert die Notwendigkeit überlasteter Funktionen.
Angenommen, Sie möchten eine Funktion schreiben, die ein Quadrat zeichnet. Diese Funktion kann einen einzigen erforderlichen Parameter sideLength haben, mit dem die Länge jeder Seite angegeben wird. Sie kann mehrere optionale Parameter wie thickness, edgeColor usw. enthalten. Wenn der Aufrufer diese nicht angibt, verwendet die Funktion Standardwerte. In anderen Sprachen werden Sie wahrscheinlich 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 ) { }
Diese Funktion erspart Ihnen nicht nur das Schreiben mehrerer redundanter Funktionen, sondern erleichtert auch das Lesen des Codes. Wenn der Aufrufer keinen Wert für ein Argument angibt, bedeutet dies, dass er zur Verwendung des Standardwerts bereit ist. Außerdem können Sie anhand der benannten Parameter den Vorgang viel leichter erkennen. 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, dasselbe für die zusammensetzbaren Funktionen zu verwenden, die Sie schreiben. Mit dieser Vorgehensweise lassen sich die zusammensetzbaren Funktionen anpassen, das Standardverhalten lässt sich aber trotzdem einfach aufrufen. Sie können beispielsweise ein einfaches Textelement wie folgt erstellen:
Text(text = "Hello, Android!")
Dieser Code hat die gleiche Wirkung wie der folgende, viel ausführlichere Code, in dem mehr der Text
-Parameter 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 selbsterklärend. Wenn Sie nur den Parameter text
angeben, dokumentieren Sie, dass Sie für alle anderen Parameter die Standardwerte verwenden möchten. Im Gegensatz dazu deutet das zweite Snippet darauf hin, dass Sie die Werte für diese anderen Parameter explizit festlegen möchten. Die von Ihnen festgelegten Werte sind jedoch die Standardwerte für die Funktion.
Funktionen höherer Ordnung und Lambda-Ausdrücke
Kotlin unterstützt höher sortierte Funktionen, also Funktionen, die andere Funktionen als Parameter empfangen. Die Erstellung baut auf diesem Ansatz auf. Die zusammensetzbare Funktion Button
stellt beispielsweise einen Lambda-Parameter onClick
bereit. 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 interagieren auf natürliche Weise mit Lambda-Ausdrücken, Ausdrücke, die als Funktion ausgewertet werden. Wenn Sie die Funktion nur einmal benötigen, müssen Sie sie nicht an anderer Stelle definieren, um sie an die höherrangige Funktion zu übergeben. Stattdessen können Sie die Funktion einfach direkt mit einem Lambda-Ausdruck definieren. Im vorherigen Beispiel wird davon ausgegangen, dass myClickFunction()
an anderer Stelle definiert ist. Wenn Sie hier jedoch nur diese Funktion 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 höherrangiger Funktionen, deren last-Parameter eine Lambda-Funktion ist. Wenn Sie einen Lambda-Ausdruck als diesen Parameter übergeben möchten, können Sie die nachgestellte Lambda-Syntax verwenden. Anstatt den Lambda-Ausdruck in die Klammern zu setzen, setzen Sie ihn dahinter. Dies ist eine häufige Situation in Compose. Sie müssen sich also mit dem Aussehen des Codes vertraut machen.
Der letzte Parameter für alle Layouts, z. B. die zusammensetzbare Funktion Column()
, ist beispielsweise content
. Diese Funktion gibt die untergeordneten UI-Elemente aus. Angenommen, Sie möchten eine Spalte mit drei Textelementen erstellen und möchten eine Formatierung 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 abrufen:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
Die beiden Beispiele haben dieselbe Bedeutung. Die geschweiften Klammern definieren den Lambda-Ausdruck, der an den Parameter content
übergeben wird.
Wenn der einzige Parameter, den Sie übergeben, ein nachgestelltes Lambda ist, also wenn der letzte Parameter ein Lambda ist und Sie keine anderen Parameter übergeben, können Sie die Klammern vollständig weglassen. Angenommen, Sie müssen keinen Modifikator an Column
übergeben. Sie könnten den Code
wie folgt schreiben:
Column { Text("Some text") Text("Some more text") Text("Last text") }
Diese Syntax kommt in Compose häufig vor, insbesondere bei Layoutelementen wie Column
. Der letzte Parameter ist ein Lambda-Ausdruck, der die untergeordneten Elemente des Elements definiert. Diese untergeordneten Elemente werden nach dem Funktionsaufruf in geschweiften Klammern angegeben.
Sucher und Empfänger
Einige Methoden und Attribute sind nur in einem bestimmten Bereich verfügbar. Aufgrund des begrenzten Umfangs können Sie Funktionen dort anbieten, wo sie benötigt werden, und vermeiden, diese Funktionen versehentlich zu verwenden, wenn sie nicht angemessen sind.
Sehen wir uns ein Beispiel aus dem Tool „Compose“ an. Wenn Sie die zusammensetzbare Layoutoption Row
aufrufen, wird Ihr Inhalts-Lambda automatisch in einem RowScope
aufgerufen.
Dadurch kann Row
Funktionen bereitstellen, die nur innerhalb eines Row
-Objekts gültig sind.
Das folgende Beispiel zeigt, wie Row
einen zeilenspezifischen Wert für den align
-Modifikator 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 Attribute und Funktionen, die basierend auf der Parameterdeklaration an anderer Stelle definiert sind:
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 Attribute
Kotlin unterstützt delegierte Attribute.
Diese Eigenschaften werden wie Felder aufgerufen, ihr Wert wird jedoch dynamisch durch die Auswertung eines Ausdrucks bestimmt. Sie erkennen diese Attribute an ihrer Verwendung der Syntax by
:
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 Properties sind besonders nützlich, wenn Sie mit staatlich unterstützten Properties arbeiten:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Datenklassen löschen
Wenn Sie eine Datenklasse definieren, können Sie ganz einfach mit einer desstrukturierenden Deklaration 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
Diese Art von Code wird in Compose-Funktionen häufig angezeigt:
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
Mit Kotlin ist es einfach, Singleton-Klassen zu deklarieren, d. h. Klassen, die immer nur eine Instanz haben. Diese Singleton-Werte werden mit dem Keyword object
deklariert.
Compose verwendet häufig solche Objekte. MaterialTheme
ist beispielsweise als Singleton-Objekt definiert. Die Attribute MaterialTheme.colors
, shapes
und typography
enthalten alle Werte für das aktuelle Thema.
Typsichere Builder und DSLs
Mit Kotlin können Sie domainspezifische Sprachen (DSLs) mit typsicheren Buildern erstellen. DSLs ermöglichen den Aufbau komplexer hierarchischer Datenstrukturen, die besser zu verwalten und lesbar sind.
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 mit Funktionsliteralen mit Empfänger.
Wenn wir die zusammensetzbare Funktion Canvas
als Beispiel nehmen, wird eine Funktion mit DrawScope
als Empfänger (onDraw: DrawScope.() -> Unit
) als Parameter verwendet, sodass der Codeblock Mitgliederfunktionen aufrufen kann, 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
Koroutinen bieten Unterstützung für asynchrone Programmierung auf Sprachebene in Kotlin. Koroutinen können die Ausführung sperren, ohne Threads zu blockieren. Eine responsive UI ist von Natur aus asynchron. Jetpack Compose löst dieses Problem, indem Koroutinen auf API-Ebene verwendet werden, anstatt Callbacks zu verwenden.
Jetpack Compose bietet APIs, die die Verwendung von Koroutinen auf der UI-Ebene sicher machen.
Die Funktion rememberCoroutineScope
gibt ein CoroutineScope
-Objekt zurück, mit dem Sie Koroutinen in Event-Handlern erstellen und Compose-Halte-APIs aufrufen können. Im folgenden Beispiel wird die animateScrollTo
API von ScrollState
verwendet.
// 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() } } ) { /* ... */ }
Koroutinen führen den Codeblock standardmäßig sequentiell aus. Eine laufende Koroutine, die eine Beendigungsfunktion aufruft, unterbricht ihre Ausführung, bis die Unterbrechungsfunktion zurückgegeben wird. Dies gilt auch dann, wenn die Sperrfunktion die Ausführung in eine andere CoroutineDispatcher
verschiebt. Im vorherigen Beispiel wird loadData
erst ausgeführt, wenn die Beendigungsfunktion animateScrollTo
zurückgegeben wird.
Um Code gleichzeitig auszuführen, müssen neue Koroutinen erstellt werden. Im obigen Beispiel sind zwei Koroutinen 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() } } ) { /* ... */ }
Koroutinen erleichtern das Kombinieren asynchroner APIs. Im folgenden Beispiel kombinieren wir den pointerInput
-Modifikator 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 Koroutinen finden Sie im Leitfaden zu Kotlin-Koroutinen für Android.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Materialkomponenten und Layouts
- Nebeneffekte in der Funktion „Schreiben“
- Grundlagen des E-Mail-Layouts