Kotlin für Jetpack Compose

Jetpack Compose basiert auf Kotlin. In einigen Fällen bietet Kotlin spezielle Redewendungen, die das Schreiben von gutem Compose-Code erleichtern. Wenn Sie in einer anderen Programmiersprache denken und diese Sprache mental in Kotlin übersetzen, gehen Ihnen wahrscheinlich einige der Stärken von Compose verloren. Außerdem fällt es Ihnen möglicherweise schwer, idiomatisch geschriebenen Kotlin-Code zu verstehen. Mehr gewinnen Wenn Sie mit dem Kotlin-Stil vertraut sind, können Sie diese Schwierigkeiten vermeiden.

Standardargumente

Wenn Sie eine Kotlin-Funktion schreiben, können Sie Standardwerte dafür angeben Argumente, 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. Das Funktion einen einzelnen erforderlichen Parameter sideLength haben, mit dem die Länge angegeben wird auf jeder Seite. Es kann mehrere optionale Parameter wie Dicke, edgeColor und so weiter. Wenn der Aufrufer diese nicht angibt, für die Funktion Standardwerte verwendet. In anderen Sprachen verschiedene Funktionen:

// 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:

fun drawSquare(
    sideLength: Int,
    thickness: Int = 2,
    edgeColor: Color = Color.Black
) {
}

Dadurch müssen Sie nicht mehrere redundante Funktionen schreiben und Ihr Code ist viel übersichtlicher. Wenn der Aufrufer kein -Wert für ein Argument, das angibt, dass sie zur Verwendung des Standardwerts Wert. Außerdem können Sie anhand der benannten Parameter aktiviert. Wenn Sie im Code einen Funktionsaufruf wie diesen sehen, wissen, was die Parameter bedeuten, ohne den drawSquare()-Code zu überprüfen:

drawSquare(30, 5, Color.Red);

Im Gegensatz dazu ist dieser Code selbstdokumentierend:

drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)

Die meisten Schreibbibliotheken verwenden Standardargumente. für die zusammensetzbaren Funktionen, die Sie schreiben. Durch diese Übung werden Ihre zusammensetzbare Funktionen anpassbar, macht das Standardverhalten aber dennoch einfach aufzurufen. So könnten Sie beispielsweise ein einfaches Textelement erstellen:

Text(text = "Hello, Android!")

Dieser Code hat denselben Effekt wie der folgende, viel ausführlichere Code, in dem mehr der Parameter von Text 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 selbstdokumentierend. Wenn Sie nur den Parameter text angeben, dokumentieren Sie das für allen anderen Parametern verwenden, möchten Sie die Standardwerte verwenden. Im Gegensatz dazu das zweite Snippet impliziert, dass Sie die Werte für diese Parameter verwendet. Die von Ihnen festgelegten Werte sind jedoch die Standardwerte für der Funktion.

Funktionen höherer Ordnung und Lambda-Ausdrücke

Kotlin unterstützt höhere Reihenfolge Funktionen, die andere Funktionen als Parameter empfangen. Die Erstellung 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 koppeln natürlich mit Lambda-Ausdrücken, Ausdrücken, 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öhere Funktion zu übergeben. Stattdessen können Sie definiere einfach die Funktion direkt mit einem Lambda-Ausdruck. Das vorherige Beispiel setzt voraus, dass myClickFunction() an anderer Stelle definiert ist. Wenn Sie diese Funktion jedoch nur hier verwenden, ist es einfacher, sie 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 ein Lambda-Parameter ist. Wenn Sie einen Lambda-Ausdruck übergeben möchten, kannst du nachgestellte Lambda- Syntax. Anstatt den Lambda-Ausdruck in die Klammern zu setzen, setzen Sie ihn . Das kommt häufig in der Funktion zum Schreiben von Text vor, Sie sollten sich also mit dem Aussehen des Codes.

Der letzte Parameter für alle Layouts, wie der Column() zusammensetzbare Funktion ist content, eine Funktion, die die untergeordnete UI ausgibt. Elemente. Angenommen, Sie möchten eine Spalte mit drei Textelementen erstellen, und müssen etwas formatieren. 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 übergeben wir seinen Wert als Lambda-Ausdruck, wir können ihn aus der Klammern:

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 die Lambda-Funktion. 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 zu setzen – Sie können die Klammern ganz weglassen. Angenommen, Sie musste keinen Modifizierer an Column übergeben. Sie könnten den Code so schreiben, dies:

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

Diese Syntax kommt in Compose recht 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. Die eingeschränkte können Sie Funktionen dort anbieten, wo sie benötigt werden, und vermeiden, wenn dies nicht angemessen ist.

Sehen wir uns ein Beispiel aus dem Tool „Compose“ an. Wenn Sie das Layout Row aufrufen zusammensetzbar ist, wird das Lambda für Inhalte automatisch in einem RowScope aufgerufen. So können Row-Elemente Funktionen bereitstellen, die nur innerhalb eines Row gültig sind. Im folgenden Beispiel wird gezeigt, wie Row einen zeilenspezifischen Wert für den Modifikator align ausgegeben 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 basierend auf der Parameterdeklaration Zugriff auf Eigenschaften und Funktionen, die 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 unter Funktionsliterale mit Empfänger in der Kotlin-Dokumentation.

Delegierte Unterkünfte

Kotlin unterstützt delegierte Eigenschaften. Diese Properties werden aufgerufen, als wären sie Felder, ihr Wert wird jedoch dynamisch durch Auswertung eines Ausdrucks ermittelt. Sie erkennen diese Eigenschaften durch 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 Attribute sind besonders nützlich, staatlich unterstützten Properties:

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 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: dies:

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 zum Beispiel definiert, definiert der Compiler automatisch nützliche Funktionen wie equals() und copy(). Weitere Informationen finden Sie in den Daten Klassen.

Singleton-Objekte

Mit Kotlin ist es einfach, Singleton-Klassen zu deklarieren, d. h. Klassen, die immer ein und nur eine Instanz. Diese Singleton-Werte werden mit dem Keyword object deklariert. In Compose werden häufig solche Objekte verwendet. Beispiel: MaterialTheme ist definiert als Singleton-Objekt; die MaterialTheme.colors, shapes und typography-Eigenschaften enthalten alle die Werte für das aktuelle Design.

Typsichere Builder und DSLs

Mit Kotlin können domainspezifische Sprachen (DSLs) erstellt werden. mit typsicheren Buildern. Mithilfe von DSLs lassen sich komplexe hierarchische Datenstrukturen übersichtlicher und leichter verwalten.

Jetpack Compose nutzt 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. Wenn wir die Canvas nehmen, zusammensetzbar ist, verwendet sie als Parameter eine Funktion mit DrawScope als Empfänger, onDraw: DrawScope.() -> Unit, sodass der Codeblock Mitgliederfunktionen 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 unter 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 Benutzeroberfläche ist von Natur aus asynchron. Jetpack Compose löst dieses Problem, indem es coroutines auf API-Ebene verwendet, anstatt Callbacks zu verwenden.

Jetpack Compose bietet APIs, die die sichere Verwendung von Tasks in der UI-Ebene ermöglichen. Die rememberCoroutineScope gibt ein CoroutineScope-Objekt zurück, mit dem Sie Koroutinen in Event-Handlern erstellen und Aussetzen von APIs erstellen Siehe das folgende Beispiel mit der ScrollStates animateScrollTo API verwenden.

// 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. Bei einer laufenden Coroutine, die eine Suspend-Funktion aufruft, wird die Ausführung angehalten, bis die Suspend-Funktion zurückkehrt. Das gilt auch, wenn die Ausführung durch die Funktion „suspend“ auf eine andere CoroutineDispatcher verschoben wird. Im vorherigen Beispiel loadData wird erst ausgeführt, wenn die Unterbrechungsfunktion animateScrollTo angewendet wird Rücksendungen.

Um Code gleichzeitig auszuführen, müssen neue Koroutinen erstellt werden. Im Beispiel um das Scrollen zum oberen Bildschirmrand und das Laden von Daten aus viewModel, zwei Koroutinen sind erforderlich.

// 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 das Display 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 findest du in der Kotlin-Koroutinen für Android