Angewandtes Wissen

Es können häufige Compose-Fehler auftreten. Diese Fehler können dazu führen, dass der Code zwar gut genug zu funktionieren scheint, aber die Leistung der Benutzeroberfläche beeinträchtigt. Befolgen Sie die Best Practices zur Optimierung Ihrer App mit Compose.

remember verwenden, um kostenintensive Berechnungen zu minimieren

Composable-Funktionen können sehr häufig ausgeführt werden, sogar für jeden Frame einer Animation. Aus diesem Grund sollten Sie so wenig Berechnungen wie möglich im Body Ihrer Composable-Funktion durchführen.

Eine wichtige Methode ist das Speichern der Ergebnisse von Berechnungen mit remember. So wird die Berechnung nur einmal ausgeführt und Sie können die Ergebnisse abrufen, wann immer Sie sie benötigen.

Hier ist beispielsweise Code, der eine sortierte Liste von Namen anzeigt, die Sortierung aber auf sehr aufwendige Weise vornimmt:

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

Jedes Mal, wenn ContactsList neu zusammengesetzt wird, wird die gesamte Kontaktliste neu sortiert, obwohl sie sich nicht geändert hat. Wenn der Nutzer in der Liste scrollt, wird die Composable immer dann neu zusammengesetzt, wenn eine neue Zeile angezeigt wird.

Um dieses Problem zu beheben, sortieren Sie die Liste außerhalb von LazyColumn und speichern Sie die sortierte Liste mit remember:

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

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

Die Liste wird jetzt nur einmal sortiert, wenn ContactList zum ersten Mal zusammengestellt wird. Wenn sich die Kontakte oder der Vergleich ändern, wird die sortierte Liste neu generiert. Andernfalls kann die zusammensetzbare Funktion die zwischengespeicherte sortierte Liste weiter verwenden.

Verzögerte Layoutschlüssel verwenden

Bei Lazy Layouts werden Elemente effizient wiederverwendet. Sie werden nur neu generiert oder zusammengesetzt, wenn es erforderlich ist. Sie können jedoch dazu beitragen, Lazy Layouts für die Neukomposition zu optimieren.

Angenommen, durch eine Nutzeraktion wird ein Element in der Liste verschoben. Angenommen, Sie zeigen eine Liste von Notizen an, die nach Änderungszeit sortiert sind, wobei die zuletzt geänderte Notiz oben steht.

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

Es gibt jedoch ein Problem mit diesem Code. Angenommen, die untere Note wird geändert. Sie ist jetzt die zuletzt geänderte Notiz und wird daher oben in der Liste angezeigt. Alle anderen Notizen werden um eine Position nach unten verschoben.

Ohne Ihre Hilfe erkennt Compose nicht, dass unveränderte Elemente nur in der Liste verschoben werden. Stattdessen geht Compose davon aus, dass das alte „Punkt 2“ gelöscht und ein neues für Punkt 3, Punkt 4 usw. erstellt wurde. Das Ergebnis ist, dass Compose jedes Element in der Liste neu zusammensetzt, obwohl sich nur eines davon tatsächlich geändert hat.

Die Lösung besteht darin, Artikel-Schlüssel anzugeben. Wenn Sie für jedes Element einen stabilen Schlüssel angeben, kann Compose unnötige Neukompositionen vermeiden. In diesem Fall kann Compose feststellen, dass es sich bei dem Element auf Position 3 jetzt um dasselbe Element handelt, das sich zuvor auf Position 2 befand. Da sich keine Daten für dieses Element geändert haben, muss Compose es 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)
        }
    }
}

Mit derivedStateOf Recompositions begrenzen

Ein Risiko bei der Verwendung von Status in Ihren Composables besteht darin, dass die Benutzeroberfläche bei schnellen Statusänderungen möglicherweise häufiger neu zusammengesetzt wird als nötig. Angenommen, Sie zeigen eine scrollbare Liste an. Sie prüfen den Status der Liste, um herauszufinden, 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 ist, dass sich listState ständig ändert, wenn der Nutzer durch die Liste scrollt. Das bedeutet, dass die Liste ständig neu zusammengestellt wird. Sie müssen die Liste jedoch nicht so oft neu zusammenstellen. Das ist erst erforderlich, wenn unten ein neues Element sichtbar wird. Das bedeutet viel zusätzliche Rechenleistung, was sich negativ auf die Leistung der Benutzeroberfläche auswirkt.

Die Lösung besteht darin, abgeleiteten Status zu verwenden. Mit abgeleitetem Status können Sie Compose mitteilen, welche Zustandsänderungen tatsächlich eine Neuzusammensetzung auslösen sollen. In diesem Fall geben Sie an, dass Sie sich dafür interessieren, wann sich das erste sichtbare Element ändert. Wenn sich der Statuswert that ändert, muss die Benutzeroberfläche neu zusammengesetzt werden. Wenn der Nutzer jedoch noch nicht weit genug gescrollt hat, um ein neues Element oben zu platzieren, muss sie 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 es helfen, das Lesen des Status zu verzögern. Durch das Verzögern von Statuslesevorgängen wird sichergestellt, dass Compose bei der Neukomposition den minimal möglichen Code noch einmal ausführt. Wenn Ihre Benutzeroberfläche beispielsweise einen Status hat, der weit oben im zusammensetzbaren Baum angehoben wird, und Sie den Status in einem untergeordneten zusammensetzbaren Element lesen, können Sie das Lesen des Status in eine Lambda-Funktion einfügen. Dadurch wird der Lesevorgang nur dann ausgeführt, wenn er tatsächlich erforderlich ist. Eine Referenzimplementierung finden Sie in der Jetsnack-Beispiel-App. In Jetsnack wird auf dem Detailbildschirm ein Effekt wie bei einer minimierbaren Symbolleiste implementiert. Weitere Informationen dazu, warum diese Technik funktioniert, finden Sie im Blogpost Jetpack Compose: Debugging Recomposition.

Um diesen Effekt zu erzielen, benötigt die Title-Composable den Scroll-Offset, um sich mit einem Modifier zu versetzen. 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 der nächstgelegene übergeordnete Bereich für die Neukomposition in Compose ungültig gemacht. In diesem Fall ist der nächste Bereich die zusammensetzbare Funktion SnackDetail. Box ist eine Inline-Funktion und daher kein Bereich für die Neuzusammenstellung. Compose führt also SnackDetail und alle Composables in SnackDetail neu aus. Wenn Sie Ihren Code so ändern, dass der Status nur dort gelesen wird, wo er tatsächlich verwendet wird, 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 eine Lambda-Funktion. Das bedeutet, dass Title weiterhin auf den angehobenen Status verweisen kann, der Wert aber nur in Title gelesen wird, wo er tatsächlich benötigt wird. Wenn sich der Scrollwert ändert, ist der nächste Recomposition-Bereich jetzt die Title-Composable. Compose muss nicht mehr die gesamte Box neu zusammensetzen.

Das ist eine gute Verbesserung, aber du kannst noch mehr erreichen! Sie sollten misstrauisch werden, wenn Sie eine Neukomposition nur auslösen, um ein Composable neu zu layouten oder neu zu zeichnen. In diesem Fall ändern Sie nur den Offset der Title-Composable, was in der Layoutphase erfolgen könnte.

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

Bisher wurde im Code Modifier.offset(x: Dp, y: Dp) verwendet, wobei der Offset als Parameter angegeben wurde. Wenn Sie zur Lambda-Version des Modifikators wechseln, wird der Scrollstatus in der Layoutphase gelesen. Wenn sich der Scrollstatus ändert, kann Compose die Kompositionsphase also vollständig überspringen und direkt zur Layoutphase übergehen. Wenn Sie häufig wechselnde Statusvariablen an Modifizierer übergeben, sollten Sie nach Möglichkeit die Lambda-Versionen der Modifizierer 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 des Felds schnell zwischen zwei Farben. Dieser Status ändert sich daher sehr häufig. Die Composable-Funktion liest diesen Status dann im Hintergrundmodifikator. Daher muss das Feld bei jedem Frame neu zusammengesetzt werden, da sich die Farbe bei jedem Frame ändert.

Um dies zu verbessern, verwenden Sie einen lambdabasierten Modifikator, in diesem Fall drawBehind. Das bedeutet, dass der Farbstatus nur während der Zeichenphase gelesen wird. Daher kann Compose die Kompositions- und Layoutphasen vollständig überspringen. Wenn sich die Farbe ändert, geht Compose direkt zur Zeichenphase über.

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

Rückwärtige Schreibvorgänge vermeiden

Compose geht davon aus, dass Sie niemals in einen Status schreiben, der bereits gelesen wurde. Dies wird als Rückwärtsschreiben bezeichnet und kann dazu führen, dass bei jedem Frame eine Neuzusammenstellung erfolgt, und zwar endlos.

Das folgende Beispiel zeigt ein Composable, in dem dieser Fehler auftritt.

@Composable
fun BadComposable() {
    var count by remember { mutableIntStateOf(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>
}

Mit diesem Code wird die Anzahl am Ende der Composable-Funktion aktualisiert, nachdem sie in der vorherigen Zeile gelesen wurde. Wenn Sie diesen Code ausführen, sehen Sie, dass der Zähler nach dem Klicken auf die Schaltfläche, die eine Neukomposition auslöst, in einer Endlosschleife schnell ansteigt, da Compose diese Composable-Funktion neu komponiert, einen veralteten Statuslesevorgang erkennt und daher eine weitere Neukomposition plant.

Sie können Rückschreibvorgänge vollständig vermeiden, indem Sie in der Komposition niemals in den Status schreiben. Schreiben Sie nach Möglichkeit immer in Reaktion auf ein Ereignis und in einem Lambda in den Status, wie im vorherigen onClick-Beispiel.

Eine ausführliche Anleitung zu Rückschreibvorgängen, Recomposition-Schleifen und Phasenkoordination finden Sie unter Rückschreibvorgänge in Compose.

Zusätzliche Ressourcen