Angewandtes Wissen

Möglicherweise stoßen Sie auf häufige Compose-Probleme. Diese Fehler können dazu führen, dass der Code zwar gut genug zu laufen scheint, aber die Leistung der Benutzeroberfläche beeinträchtigt wird. Folgen Sie den Best Practices, um Ihre App in Compose zu optimieren.

remember verwenden, um rechenintensive Berechnungen zu minimieren

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

Eine wichtige Technik ist das Speichern der Ergebnisse von Berechnungen mit remember. So wird die Berechnung einmal ausgeführt und Sie können die Ergebnisse bei Bedarf abrufen.

Hier ist beispielsweise ein 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 sich die Liste nicht geändert hat. Wenn der Nutzer durch die Liste scrollt, wird das Composable neu zusammengesetzt, sobald eine neue Zeile erscheint.

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) {
            // ...
        }
    }
}

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

Übergeben Sie alle Eingaben, auf die im Berechnungsblock remember verwiesen wird, immer als Schlüssel, damit der im Cache gespeicherte Wert aktualisiert wird, wenn sich diese Eingaben ändern. Da remember die Schlüssel und das im Cache gespeicherte Ergebnis in der Slot-Tabelle speichert und die Schlüssel bei jedem Neuzusammensetzungsdurchlauf mit Object.equals() vergleicht, sollten Sie remember nur verwenden, wenn Sie aufwendige Berechnungen oder umfangreiche Objektzuweisungen (z. B. Datumsformatierer, Shader oder Listensortierung) im Cache speichern oder wenn die Objektidentität beibehalten werden muss.

Seien Sie vorsichtig, wenn Sie eine große Sammlung oder ein Objekt mit einer aufwendigen equals()-Implementierung als Schlüssel an remember für eine einfache Berechnung übergeben, da der Vergleich des Schlüssels mehr kosten kann, als die Berechnung direkt auszuführen:

@Composable
fun ContactHeader(contacts: List<Contact>) {
    // Avoid: Comparing the `contacts` list with .equals() on recomposition
    // is more expensive than reading contacts.size or contacts.firstOrNull() directly.
    val firstContactName = remember(contacts) {
        contacts.firstOrNull()?.name.orEmpty()
    }
    Text(firstContactName)
}

Lazy-Layout-Schlüssel und Inhaltstypen verwenden

Lazy Layouts verwenden Elemente effizient wieder und generieren oder rekomponieren sie nur, wenn es nötig ist. Sie können jedoch dazu beitragen, Lazy-Layouts für die Neuzusammensetzung und die Wiederverwendung von Slots zu optimieren.

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

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

Dieser Code hat jedoch ein Problem. Angenommen, die untere Note wird geändert. Sie ist jetzt die zuletzt geänderte Notiz und wird daher ganz 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 in der Liste nur verschoben werden. Stattdessen geht Compose davon aus, dass das alte „Element 2“ gelöscht und ein neues für Element 3, Element 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, Elementschlüssel anzugeben. Wenn Sie für jedes Element einen stabilen Schlüssel angeben, kann Compose unnötige Neuzusammensetzungen vermeiden. In diesem Fall kann Compose feststellen, dass das Element an Position 3 dasselbe Element ist, das sich zuvor an Position 2 befand. Da sich keine der 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)
        }
    }
}

Wenn Sie mit Lazy Layouts (LazyColumn, LazyRow, LazyGrid) arbeiten, sollten Sie außerdem die folgenden Richtlinien beachten:

  • Verwenden Sie eindeutige Schlüssel, die in einem Bundle gespeichert werden können: Schlüssel müssen eindeutig, deterministisch und in einem Android-Bundle speicherbar sein, da Lazy Layouts intern rememberSaveable verwenden. Bevorzugen Sie einfache Typen wie Int, Long oder String und verwenden Sie niemals Listenindexe oder hashCode() als Schlüssel.
  • Geben Sie für heterogene Listen immer contentType an: Übergeben Sie contentType = { it.type } an items oder itemsIndexed, damit Compose beim Scrollen nur Kompositions-Slots zwischen Elementen mit derselben visuellen Struktur wiederverwendet.
  • itemsIndexed anstelle von indexOf verwenden: Vermeiden Sie den Aufruf von list.indexOf(item) innerhalb eines items-Blocks, da dadurch das Layout in einen O(N²)-Scan umgewandelt wird. Verwenden Sie stattdessen itemsIndexed.
  • Gemeinsamen Status und CompositionLocal-Lesevorgänge hochstufen: Erfassen Sie freigegebene Flow-Instanzen und lesen Sie CompositionLocal-Werte (z. B. LocalContext.current oder LocalDensity.current) einmal in der übergeordneten zusammensetzbaren Funktion über dem Lazy-Layout und übergeben Sie die aufgelösten Werte an die zusammensetzbaren Elemente.
  • Row oder Column für kleine, begrenzte Listen verwenden: Bei kleinen statischen oder begrenzten Sammlungen (z. B. 1 bis 10 Elemente) wird durch eine Standard-Row oder -Column mit forEach der Overhead für Lazy Virtualization und Prefetching vermieden.

computedStateOf oder derivedStateOf verwenden, um Neuzusammensetzungen zu begrenzen

Ein Risiko bei der Verwendung des Status in Ihren Kompositionen besteht darin, dass sich die Benutzeroberfläche bei schnellen Statusänderungen möglicherweise häufiger als nötig neu zusammensetzt. 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 ist, dass sich listState beim Scrollen durch die Liste ständig ändert, wenn der Nutzer den Finger bewegt. Das bedeutet, dass die Liste ständig neu zusammengestellt wird. Sie müssen sie jedoch nicht so oft neu zusammensetzen. Eine Neuzusammensetzung ist erst erforderlich, wenn ein neues Element oben sichtbar wird. Das sind viele zusätzliche Berechnungen, die die Leistung der Benutzeroberfläche beeinträchtigen.

Die Lösung besteht darin, den abgeleiteten Zustand (oder computedStateOf in Compose Runtime 1.13 und höher) zu verwenden. Mit dem abgeleiteten Zustand können Sie Compose mitteilen, welche Zustandsänderungen tatsächlich eine Neuzusammensetzung auslösen sollen. In diesem Fall geben Sie an, dass Sie wissen möchten, wann sich das erste sichtbare Element ändert. Wenn sich dieser Zustandswert ändert, muss die UI neu zusammengesetzt werden. Wenn der Nutzer jedoch noch nicht weit genug gescrollt hat, um ein neues Element nach oben zu bringen, ist keine Neuzusammensetzung erforderlich.

val listState = rememberLazyListState()

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

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

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Wählen Sie je nach Compose-Version und Berechnungskosten den geeigneten Ansatz zur Ableitung des Zustands aus:

  • remember { computedStateOf { ... } } (Compose Runtime 1.13 und höher): Die Berechnung wird bei jedem Lesevorgang neu ausgewertet, ohne dass das Ergebnis im Cache gespeichert wird. Die Komposition wird nur dann ungültig, wenn sich die berechnete Ausgabe ändert. Da remember { computedStateOf { listState.firstVisibleItemIndex > 0 } } die Cache-Buchführung und den Invalidierungs-Write-Back-Overhead vermeidet, sollten Sie es für grundlegende Bedingungsprüfungen und einfache Ableitungen verwenden, die durch sich schnell ändernde Zustände (z. B. Scroll-Offsets) gesteuert werden.
  • remember { derivedStateOf { ... } }: Speichert das Ergebnis der Berechnung über mehrere Lesevorgänge hinweg im Cache. Verwenden Sie derivedStateOf für Compose-Versionen vor 1.13 oder wenn die Berechnung selbst rechenintensiv ist und während eines Kompositionsdurchlaufs mehrmals gelesen wird. Übergeben Sie die in derivedStateOf gelesenen Snapshot-Zustandswerte (z. B. listState.firstVisibleItemIndex) nicht als remember-Schlüssel, da dadurch bei jeder Zustandsänderung die DerivedState neu erstellt und das Überspringen verhindert wird.
  • Einfache Kotlin-Getter (get()) und direktes Lesen von Properties: Verwenden Sie für einfache boolesche Ausdrücke, bei denen keine Dämpfung der Invalidierung erforderlich ist, einen einfachen benutzerdefinierten Getter und lesen Sie die Sammlung .size direkt, anstatt sie in derivedStateOf zu verpacken. Vermeiden Sie die Verwendung von produceState mit snapshotFlow für synchrone Statusableitungen, da dies unnötigen Overhead für die Coroutine-Verteilung verursacht.

Lesevorgänge so lange wie möglich aufschieben

Wenn ein Leistungsproblem erkannt wurde, kann es helfen, das Lesen des Status zu verschieben. Wenn Sie das Lesen des Status verzögern, wird bei der Neuzusammensetzung nur der minimal erforderliche Code ausgeführt. Wenn Ihre UI beispielsweise einen Status hat, der im Baum der zusammensetzbaren Funktionen weit oben liegt, und Sie den Status in einer untergeordneten zusammensetzbaren Funktion lesen, können Sie den Statuslesevorgang in eine Lambda-Funktion einbinden. Dadurch wird der Lesevorgang nur dann ausgeführt, wenn er tatsächlich erforderlich ist. Eine Referenz finden Sie in der Implementierung in der Jetsnack-Beispiel-App. Jetsnack implementiert auf dem Detailbildschirm einen Effekt, der einer minimierbaren Symbolleiste ähnelt. Informationen dazu, warum diese Technik funktioniert, finden Sie im Blogpost Jetpack Compose: Debugging Recomposition.

Um diesen Effekt zu erzielen, benötigt die komponierbare Funktion Title den Scroll-Offset, um sich selbst mit einem 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 in Compose der nächstgelegene übergeordnete Neuzusammensetzungsbereich ungültig. In diesem Fall ist der nächstgelegene Bereich die zusammensetzbare Funktion SnackDetail. Box ist eine Inline-Funktion und daher kein Rekompositionsbereich. Compose setzt also SnackDetail und alle Composables innerhalb von SnackDetail neu zusammen. Wenn Sie Ihren Code so ändern, dass der Status nur dort gelesen wird, wo 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 Scrollparameter ist jetzt ein Lambda. Das bedeutet, dass Title weiterhin auf den hochgezogenen Status verweisen kann, der Wert aber nur innerhalb von Title gelesen wird, wo er tatsächlich benötigt wird. Wenn sich der Scrollwert ändert, ist der nächste Rekompositionsbereich jetzt die zusammensetzbare Funktion Title. Compose muss nicht mehr die gesamte Box neu zusammensetzen.

Das ist eine gute Verbesserung, aber du kannst es noch besser machen! Sie sollten misstrauisch werden, wenn Sie eine Neuzusammensetzung nur auslösen, um ein Composable neu anzuordnen oder neu zu zeichnen. In diesem Fall ändern Sie nur den Offset des Composables Title, was in der Layoutphase möglich ist.

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

Zuvor wurde im Code Modifier.offset(x: Dp, y: Dp) verwendet, wobei 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 Kompositionsphase vollständig überspringen und direkt zur Layoutphase übergehen. Wenn Sie häufig wechselnde Statusvariablen an 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 des Felds schnell zwischen zwei Farben. Dieser Status ändert sich also sehr häufig. Das Composable 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 Lambda-basierten Modifikator, in diesem Fall drawBehind. Das bedeutet, dass der Farbstatus nur während der Zeichenphase gelesen wird. Daher kann Compose die Phasen „Komposition“ und „Layout“ vollständig überspringen. Wenn sich die Farbe ändert, geht Compose direkt zur Phase „Zeichnen“ über.

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

Rückwärtsschreibvorgänge vermeiden

Compose geht davon aus, dass Sie niemals in einen Status schreiben, der bereits gelesen wurde. In diesem Fall spricht man von einem Rückwärtsschreiben. Dies kann dazu führen, dass die Neuzusammensetzung bei jedem Frame endlos erfolgt.

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

@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>
}

Dieser Code aktualisiert die Anzahl am Ende des Kompositionselements, nachdem sie in der vorherigen Zeile gelesen wurde. Wenn Sie diesen Code ausführen, werden Sie feststellen, dass sich der Zähler nach dem Klicken auf die Schaltfläche, die eine Neuzusammensetzung auslöst, in einer Endlosschleife schnell erhöht, da Compose dieses Composable neu zusammensetzt, einen veralteten Status liest und daher eine weitere Neuzusammensetzung plant.

Sie können Rückwärtsschreibvorgänge vollständig vermeiden, indem Sie in der Composition nie in den Status schreiben. Wenn möglich, sollten Sie immer als Reaktion auf ein Ereignis und in einem Lambda wie im vorherigen onClick-Beispiel in den Status schreiben.

Eine ausführliche Anleitung zu Rückwärtsschreibvorgängen, Neuzusammensetzungsschleifen und Phasenkoordination finden Sie unter Rückwärtsschreibvorgänge in Compose.

Zusätzliche Ressourcen