Angewandtes Wissen

Beim Schreiben können häufige Fehler auftreten. Durch diese Fehler erhalten Sie möglicherweise Code, der gut genug zu funktionieren scheint, aber die Leistung der Benutzeroberfläche beeinträchtigen kann. Folgen Sie den Best Practices, um Ihre Anwendung in Compose zu optimieren.

remember verwenden, um teure Berechnungen zu minimieren

Zusammensetzbare Funktionen können sehr häufig ausgeführt werden, so oft wie für jeden Frame einer Animation. Aus diesem Grund sollten Sie im Text der zusammensetzbaren Funktion so wenig Berechnungen wie möglich durchführen.

Ein wichtiges Verfahren besteht darin, die Ergebnisse der Berechnungen mit remember zu speichern. Auf diese Weise wird die Berechnung einmal ausgeführt und Sie können die Ergebnisse bei Bedarf abrufen.

Im Folgenden finden Sie zum Beispiel Code, der eine sortierte Liste von Namen anzeigt, die Sortierung jedoch sehr aufwendig erfolgt:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

Bei jeder Neuzusammensetzung von ContactsList wird die gesamte Kontaktliste neu sortiert, auch wenn sich die Liste nicht geändert hat. Scrollt der Nutzer durch die Liste, wird die zusammensetzbare Funktion jedes Mal neu zusammengesetzt, wenn eine neue Zeile erscheint.

Sortieren Sie die Liste außerhalb von LazyColumn und speichern Sie die sortierte Liste mit remember, um dieses Problem zu lösen:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

Jetzt wird die Liste einmal sortiert, wenn ContactList zum ersten Mal zusammengesetzt wird. Wenn sich die Kontakte oder der Vergleichsoperator ändern, wird die sortierte Liste neu generiert. Andernfalls kann die zusammensetzbare Funktion weiterhin die im Cache gespeicherte sortierte Liste verwenden.

Lazy-Layout-Tasten verwenden

In verzögerten Layouts können Sie Elemente effizient wiederverwenden und sie nur bei Bedarf neu generieren oder neu zusammenstellen. Sie können jedoch auch Lazy Layouts für die Neuzusammensetzung optimieren.

Angenommen, ein Nutzervorgang führt dazu, dass ein Element in der Liste verschoben wird. Angenommen, Sie zeigen eine nach Änderungszeit sortierte Liste von Notizen an, wobei der zuletzt geänderte Hinweis ganz oben angezeigt wird.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

Mit diesem Code gibt es jedoch ein Problem. Angenommen, die untere Notiz wird geändert. Sie ist jetzt die zuletzt geänderte Notiz und wird in der Liste ganz oben angezeigt, alle anderen Noten eine Stelle nach unten.

Ohne Ihre Hilfe erkennt das Tool nicht, dass unveränderte Elemente in der Liste nur verschoben werden. Stattdessen geht die Funktion von „Compose“ davon aus, dass das alte „Element 2“ gelöscht und für Element 3, Element 4 und ganz unten ein neuer erstellt wurde. Das hat zur Folge, dass bei der Funktion „Compose“ jedes Element auf der Liste neu zusammensetzt, obwohl nur eines davon geändert wurde.

Die Lösung besteht darin, Artikelschlüssel anzugeben. Wenn Sie für jedes Element einen stabilen Schlüssel angeben, vermeiden Sie in der Funktion „Compose“ unnötige Neuzusammensetzungen. In diesem Fall kann die Funktion „Compose“ feststellen, dass das Element an Punkt 3 mit dem Element an Punkt 2 identisch ist. Da sich die Daten für dieses Element nicht geändert haben, müssen Sie es in der Funktion nicht neu zusammensetzen.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

derivedStateOf verwenden, um Neuzusammensetzungen einzuschränken

Ein Risiko bei der Verwendung des Zustands in Ihren Kompositionen besteht darin, dass die UI möglicherweise mehr neu zusammengesetzt wird, als Sie benötigen, wenn sich der Status schnell ändert. Angenommen, Sie zeigen eine scrollbare Liste an. Sie prüfen den Status der Liste, um zu sehen, welches Element das erste sichtbare Element in der Liste ist:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Das Problem hier ist, dass sich listState ständig ändert, wenn der Nutzer durch die Liste scrollt, während er den Finger zieht. Das heißt, die Liste wird ständig neu zusammengesetzt. Sie müssen sie jedoch nicht so oft neu zusammensetzen. Sie müssen sie erst dann neu zusammensetzen, wenn ein neues Element am unteren Rand angezeigt wird. Das ist also eine Menge zusätzlicher Rechenleistung, die die Leistung Ihrer UI beeinträchtigt.

Die Lösung ist die Verwendung eines abgeleiteten Zustands. Mit dem abgeleiteten Status können Sie in der Funktion „Compose“ festlegen, welche Statusänderungen die Neuzusammensetzung auslösen sollen. Legen Sie in diesem Fall fest, dass es wichtig ist, dass sich das erste sichtbare Element ändert. Wenn sich dieser Statuswert ändert, muss sich die Benutzeroberfläche neu zusammensetzen. Wenn der Nutzer jedoch noch nicht genug gescrollt hat, um ein neues Element an den Anfang zu bringen, muss die Benutzeroberfläche nicht neu zusammengesetzt werden.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Lesevorgänge so lange wie möglich aufschieben

Wenn ein Leistungsproblem erkannt wurde, kann das Verschieben von Statuslesevorgängen hilfreich sein. Wenn Sie Statuslesevorgänge zurückstellen, wird bei der Funktion „Compose“ bei der Neuzusammensetzung der minimal mögliche Code noch einmal ausgeführt. Wenn Ihre UI beispielsweise einen Status enthält, der weit oben in der zusammensetzbaren Struktur gezogen wird, und Sie den Status in einer untergeordneten zusammensetzbaren Funktion lesen, können Sie den gelesenen Status in eine Lambda-Funktion einbinden. Dadurch wird der Lesevorgang nur dann ausgeführt, wenn er tatsächlich erforderlich ist. Sehen Sie sich hierzu die Implementierung in der Jetsnack-Beispiel-App an. Jetsnack implementiert auf dem Detailbildschirm einen Effekt wie eine minimierbare Symbolleiste. Informationen zur Funktionsweise dieser Technik finden Sie im Blogpost Jetpack Compose: Debugging Recomposition.

Um diesen Effekt zu erzielen, benötigt die zusammensetzbare Funktion Title den Scrollversatz, um sich selbst mithilfe eines Modifier zu verschieben. Hier ist eine vereinfachte Version des Jetsnack-Codes vor der Optimierung:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Wenn sich der Scrollstatus ändert, wird durch „Compose“ der nächstgelegene übergeordnete Bereich für die Neuzusammensetzung ungültig. In diesem Fall ist der nächste Bereich die zusammensetzbare Funktion SnackDetail. Beachten Sie, dass Box eine Inline-Funktion und daher kein Bereich für die Neuzusammensetzung ist. Bei der Funktion „Compose“ werden also SnackDetail und alle zusammensetzbaren Funktionen in SnackDetail neu zusammengesetzt. Wenn Sie Ihren Code so ändern, dass nur der Status gelesen wird, in dem Sie ihn tatsächlich verwenden, können Sie die Anzahl der Elemente reduzieren, die neu zusammengesetzt werden müssen.

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Der Scroll-Parameter ist jetzt ein Lambda. Das bedeutet, dass Title weiterhin auf den hochgezogenen Zustand verweisen kann, der Wert aber nur innerhalb von Title gelesen wird, wo er tatsächlich benötigt wird. Wenn sich der Scrollwert ändert, ist daher der nächste Bereich für die Neuzusammensetzung die zusammensetzbare Funktion Title. Das gesamte Box-Element muss nicht mehr neu zusammengesetzt werden.

Das ist eine gute Verbesserung, aber Sie können es noch besser! Seien Sie misstrauisch, wenn Sie die Neuzusammensetzung nur dazu veranlassen, das Layout neu zu erstellen oder eine zusammensetzbare Funktion neu zu zeichnen. In diesem Fall ändern Sie lediglich den Offset der zusammensetzbaren Funktion Title, was in der Layoutphase möglich sein könnte.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

Früher wurde in dem Code Modifier.offset(x: Dp, y: Dp) verwendet, bei dem der Offset als Parameter verwendet wird. Wenn Sie zur Lambda-Version des Modifikators wechseln, können Sie dafür sorgen, dass die Funktion den Scrollstatus in der Layoutphase liest. Wenn sich der Scrollstatus ändert, kann „Compose“ die Zusammensetzungsphase vollständig überspringen und direkt mit der Layoutphase fortfahren. Wenn Sie häufig ändernde Statusvariablen in Modifikatoren übergeben, sollten Sie nach Möglichkeit die Lambda-Versionen der Modifikatoren verwenden.

Hier ist ein weiteres Beispiel für diesen Ansatz. Dieser Code wurde noch nicht optimiert:

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

Hier wechselt die Hintergrundfarbe der Box schnell zwischen zwei Farben. Dieser Status ändert sich daher sehr häufig. Die zusammensetzbare Funktion liest diesen Status dann im Hintergrundmodifikator. Daraus folgt, dass sich das Feld auf jedem Frame neu zusammensetzen muss, da sich die Farbe mit jedem Frame ändert.

Um dies zu verbessern, verwende einen Lambda-basierten Modifikator – in diesem Fall drawBehind. Das bedeutet, dass der Farbstatus nur während der Zeichenphase gelesen wird. Dadurch kann die Erstellungs- und Layoutphase vollständig übersprungen werden. Wenn sich die Farbe ändert, geht die Erstellung direkt in die Zeichenphase über.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

Rückwärtsschreibvorgänge vermeiden

Beim Compose wird davon ausgegangen, dass Sie niemals in einen Status schreiben, der bereits gelesen wurde. Dies wird als Rückwärtsschreibvorgang bezeichnet und kann endlos zu einer Neuzusammensetzung in jedem Frame führen.

Die folgende zusammensetzbare Funktion zeigt ein Beispiel für diese Art von Fehler.

@Composable
fun BadComposable() {
    var count by remember { mutableStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

Dieser Code aktualisiert die Anzahl am Ende der zusammensetzbaren Funktion, nachdem sie aus der vorangehenden Zeile gelesen wurde. Wenn Sie diesen Code ausführen, werden Sie feststellen, dass nach dem Klicken auf die Schaltfläche der Zähler schnell in einer unendlichen Schleife zunimmt, wenn die Funktion „Compose“ diese zusammensetzbare Funktion umsetzt, einen veralteten Lesezustand erkennt und eine weitere Neuzusammensetzung plant.

Sie können umgekehrte Schreibvorgänge ganz vermeiden, wenn Sie nie für die Komposition schreiben. Wenn möglich, schreiben Sie immer in den Zustand als Reaktion auf ein Ereignis und in einem Lambda, wie im vorherigen onClick-Beispiel.

Weitere Ressourcen