Kotlin für Jetpack Compose

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 vertrauter machen, können Sie diese Fallstricke vermeiden.

Standardargumente

Wenn Sie eine Kotlin-Funktion schreiben, können Sie Standardwerte für Funktions argumente, angeben, die verwendet werden, wenn der Aufrufer diese Werte nicht explizit übergibt. Diese Funktion reduziert die Notwendigkeit überladener Funktionen.

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 haben, z. B. thickness, edgeColor usw. Wenn der Aufrufer diese nicht angibt, verwendet die Funktion Standardwerte. In anderen Sprachen müssten 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
) {
}

Diese Funktion spart Ihnen nicht nur das Schreiben mehrerer redundanter Funktionen, sondern macht Ihren Code auch viel übersichtlicher. Wenn der Aufrufer keinen Wert für ein Argument angibt, bedeutet das, dass er bereit ist, den Standardwert zu verwenden. Außerdem machen die benannten Parameter viel deutlicher, 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);

Dieser Code ist dagegen selbsterklärend:

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 zusammensetzbaren Funktionen anpassen, aber das Standardverhalten lässt sich trotzdem einfach aufrufen. Sie können beispielsweise ein einfaches Textelement so erstellen:

Text(text = "Hello, Android!")

Dieser Code hat dieselbe Wirkung wie der folgende, viel ausführlichere Code, in dem mehr Text Parameter explizit festgelegt sind:

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. Das zweite Snippet impliziert dagegen, dass Sie die Werte für diese anderen Parameter explizit festlegen möchten, obwohl die von Ihnen festgelegten Werte zufällig 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 erhalten. Compose baut auf diesem Ansatz auf. Beispielsweise bietet die zusammensetzbare Funktion Button einen onClick Lambda-Parameter. Der Wert dieses Parameters ist eine Funktion, die vom Button aufgerufen wird, wenn der Nutzer darauf klickt:

Button(
    // ...
    onClick = myClickFunction
)
// ...

Funktionen höherer Ordnung passen gut zu Lambda-Ausdrücken, also 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 einfach 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, sie mit einem Lambda-Ausdruck inline zu definieren:

Button(
    // ...
    onClick = {
        // do something
        // do something else
    }
) { /* ... */ }

Nachgestellte Lambdas

Kotlin bietet eine spezielle Syntax für den Aufruf von Funktionen höherer Ordnung, deren letzter Parameter ein Lambda ist. Wenn Sie einen Lambda-Ausdruck als diesen Parameter übergeben möchten, können Sie die Syntax für nachgestellte Lambdas verwenden. Statt den Lambda-Ausdruck in die Klammern zu setzen, setzen Sie ihn danach. Das ist in Compose eine häufige Situation, daher müssen Sie wissen, wie der Code aussieht.

Der letzte Parameter für alle Layouts, z. B. die Column() zusammensetzbare Funktion, 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 herausnehmen:

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, dieses 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 dem Column keinen Modifikator übergeben. Dann können Sie den Code so schreiben:

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

Diese Syntax ist in Compose sehr häufig, insbesondere für Layout-Elemente 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 Properties sind nur in einem bestimmten Bereich verfügbar. Durch den eingeschränkten Bereich können Sie Funktionen dort anbieten, wo sie benötigt werden, und vermeiden, dass sie versehentlich dort verwendet werden, wo sie nicht geeignet sind.

Sehen wir uns ein Beispiel an, das in Compose verwendet wird. Wenn Sie die Row Layout zusammensetzbare Funktion aufrufen, wird Ihr Inhalts-Lambda automatisch in einem RowScope aufgerufen. So kann Row Funktionen bereitstellen, die nur innerhalb eines Row gültig sind. Das folgende Beispiel zeigt, 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 Properties 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 Function literals with receiver.

Delegierte Properties

Kotlin unterstützt delegierte Properties. Diese Properties werden so aufgerufen, als wären sie Felder, aber ihr Wert wird dynamisch durch Auswerten eines Ausdrucks bestimmt. Sie erkennen diese Properties an der Verwendung der by-Syntax:

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

Anderer Code kann mit diesem 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 zustandsgestützten Properties arbeiten:

var showDialog by remember { mutableStateOf(false) }

// Updating the var automatically triggers a state change
showDialog = true

remember

Datenklassen dekonstruieren

Wenn Sie eine Daten klasse definieren, können Sie mit einer Dekonstruktions deklaration 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 diesem Code auf seine Werte zugreifen:

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

Diese Art von Code sehen Sie häufig in Compose-Funktionen:

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 Daten klassen.

Singleton-Objekte

In Kotlin lassen sich Singletons einfach deklarieren, also Klassen, die immer genau eine Instanz haben. Diese Singletons werden mit dem object Keyword deklariert. Compose verwendet solche Objekte häufig. Beispielsweise ist MaterialTheme als Singleton-Objekt definiert. Die Properties 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. DSLs ermöglichen das Erstellen komplexer hierarchischer Datenstrukturen auf wartungsfreundlichere und lesbarere Weise.

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 die Canvas zusammensetzbare Funktion als Beispiel. Sie verwendet als Parameter eine Funktion mit DrawScope als Empfänger, onDraw: DrawScope.() -> Unit. So 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

Koroutinen bieten Unterstützung für asynchrone Programmierung auf Sprachebene in Kotlin. Koroutinen können die Ausführung unterbrechen , ohne Threads zu blockieren. Eine reaktionsschnelle UI ist von Natur aus asynchron. Jetpack Compose löst dieses Problem, indem es Koroutinen auf API-Ebene verwendet, anstatt Callbacks zu verwenden.

Jetpack Compose bietet APIs, mit denen Koroutinen sicher in der UI-Ebene verwendet werden können. Die rememberCoroutineScope Funktion gibt einen CoroutineScope zurück, mit dem Sie Koroutinen in Ereignishandlern erstellen und Compose-Suspend-APIs aufrufen können. Sehen Sie sich das folgende Beispiel mit der ScrollState's animateScrollTo API an.

// 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 sequenziell aus. Eine laufende Koroutine, die eine Suspend-Funktion aufruft, unterbricht ihre Ausführung, bis die Suspend-Funktion zurückkehrt. Das gilt auch dann, wenn die Suspend-Funktion die Ausführung zu einem anderen CoroutineDispatcher verschiebt. Im vorherigen Beispiel wird loadData erst ausgeführt, wenn die Suspend-Funktion animateScrollTo zurückkehrt.

Um Code gleichzeitig auszuführen, müssen neue Koroutinen erstellt werden. Im obigen Beispiel sind zwei Koroutinen erforderlich, um das Scrollen nach oben auf dem Bildschirm 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 die Kombination asynchroner APIs. 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 and animate
                        // in the same block
                        awaitPointerEventScope {
                            val offset = 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 unter Android.