Stabilitätsprobleme beheben

Wenn eine instabile Klasse zu Leistungsproblemen führt, sollten Sie sie stabilisieren. In diesem Dokument werden verschiedene Techniken beschrieben, die Sie dazu verwenden können.

Klasse unveränderlich machen

Sie sollten zuerst versuchen, eine instabile Klasse vollständig unveränderlich zu machen.

  • Unveränderlich: Gibt einen Typ an, bei dem sich der Wert von Attributen nie mehr ändern kann, nachdem eine Instanz dieses Typs erstellt wurde. Außerdem sind alle Methoden referenziell transparent.
    • Alle Attribute der Klasse müssen sowohl val als auch var sein und unveränderliche Typen haben.
    • Primitive Typen wie String, Int und Float sind immer unveränderlich.
    • Wenn dies nicht möglich ist, müssen Sie für alle änderbaren Attribute den Status „Compose“ verwenden.
  • Stabil: Gibt einen Typ an, der änderbar ist. Die Compose-Laufzeit erkennt nicht, ob und wann die öffentlichen Attribute oder das Methodenverhalten des Typs zu anderen Ergebnissen für einen vorherigen Aufruf führen würden.

Unveränderliche Sammlungen

Ein häufiger Grund dafür, dass die Klasse „Compose“ als instabil einstuft, sind Sammlungen. Wie auf der Seite Stabilitätsprobleme diagnostizieren erwähnt, kann der Compose-Compiler nicht ganz sicher sein, ob Sammlungen wie List, Map und Set wirklich unveränderlich sind und daher als instabil gekennzeichnet werden.

Um dieses Problem zu lösen, können Sie unveränderliche Sammlungen verwenden. Der Compose-Compiler unterstützt Kotlinx-unveränderliche Sammlungen. Diese Sammlungen sind garantiert unveränderlich und der Compose-Compiler behandelt sie als solche. Diese Bibliothek befindet sich noch in der Alphaphase. Sie sollten daher mit möglichen Änderungen an der API rechnen.

Sehen Sie sich noch einmal diese instabile Klasse aus der Anleitung Stabilitätsprobleme diagnostizieren an:

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

Sie können tags mithilfe einer unveränderlichen Sammlung stabil machen. Ändern Sie in der Klasse den Typ von tags in ImmutableSet<String>:

data class Snack{
    …
    val tags: ImmutableSet<String> = persistentSetOf()
    …
}

Danach sind alle Parameter der Klasse unveränderlich und der Compose-Compiler markiert die Klasse als stabil.

Mit Stable oder Immutable annotieren

Eine mögliche Lösung zur Behebung von Stabilitätsproblemen besteht darin, instabile Klassen entweder mit @Stable oder @Immutable zu annotieren.

Durch das Annotieren einer Klasse wird überschrieben, was der Compiler sonst über die Klasse ableiten würde. Er ähnelt dem Operator !! in Kotlin. Sie sollten bei der Verwendung dieser Anmerkungen sehr vorsichtig sein. Wenn Sie das Compiler-Verhalten überschreiben, kann es zu unerwarteten Fehlern kommen, z. B. dass Ihre zusammensetzbare Funktion nicht wie erwartet neu zusammengesetzt wird.

Wenn es möglich ist, Ihre Klasse ohne Annotation stabil zu machen, sollten Sie auf diese Weise Stabilität erreichen.

Das folgende Snippet enthält ein minimales Beispiel für eine Datenklasse, die als unveränderlich annotiert ist:

@Immutable
data class Snack(
…
)

Unabhängig davon, ob Sie die Annotation @Immutable oder @Stable verwenden, markiert der Compose-Compiler die Snack-Klasse als stabil.

Annotierte Klassen in Sammlungen

Hier sehen Sie eine zusammensetzbare Funktion, die einen Parameter vom Typ List<Snack> enthält:

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  …
  unstable snacks: List<Snack>
  …
)

Auch wenn Sie Snack mit @Immutable annotieren, markiert der Compose-Compiler den snacks-Parameter in HighlightedSnacks als instabil.

Parameter haben in Bezug auf Sammlungstypen dasselbe Problem wie Klassen: Der Compiler Compose markiert einen Parameter des Typs List immer als instabil, auch wenn es sich um eine Sammlung stabiler Typen handelt.

Es ist nicht möglich, einzelne Parameter als stabil zu markieren oder eine zusammensetzbare Funktion so zu annotieren, dass sie immer überspringbar ist. Es gibt mehrere Pfade.

Es gibt mehrere Möglichkeiten, das Problem instabiler Sammlungen zu umgehen. In den folgenden Unterabschnitten werden diese verschiedenen Ansätze beschrieben.

Konfigurationsdatei

Wenn Sie den Stabilitätsvertrag in Ihrer Codebasis einhalten möchten, können Sie Kotlin-Sammlungen als stabil betrachten, indem Sie kotlin.collections.* in die Stabilitätskonfigurationsdatei einfügen.

Unveränderliche Sammlung

Um die Sicherheit bei der Kompilierungszeit und die Unveränderlichkeit zu gewährleisten, können Sie anstelle von List eine unveränderliche kotlinx-Sammlung verwenden.

@Composable
private fun HighlightedSnacks(
    …
    snacks: ImmutableList<Snack>,
    …
)

Wrapper

Wenn Sie keine unveränderliche Sammlung verwenden können, können Sie eine eigene Sammlung erstellen. Dazu verpacken Sie den List in eine annotierte stabile Klasse. Ein generischer Wrapper ist abhängig von Ihren Anforderungen wahrscheinlich die beste Wahl dafür.

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

Diesen können Sie dann als Parametertyp in Ihrer zusammensetzbaren Funktion verwenden.

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

Die Lösung

Nach einem dieser Ansätze markiert der Compose-Compiler die zusammensetzbare Funktion HighlightedSnacks als skippable und restartable.

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Bei der Neuzusammensetzung kann HighlightedSnacks jetzt übersprungen werden, wenn sich keine Eingabe geändert hat.

Stabilitätskonfigurationsdatei

Ab Compose Compiler 1.5.5 kann während der Kompilierung eine Konfigurationsdatei mit Klassen bereitgestellt werden, die als stabil gelten sollen. Dadurch können Sie Klassen, die Sie nicht steuern können, z. B. Standardbibliotheksklassen wie LocalDateTime, als stabil betrachten.

Die Konfigurationsdatei ist eine Nur-Text-Datei mit einer Klasse pro Zeile. Kommentare sowie einfache und doppelte Platzhalter werden unterstützt. Hier eine Beispielkonfiguration:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

Übergeben Sie den Pfad der Konfigurationsdatei an die Compiler-Optionen „Compose“, um dieses Feature zu aktivieren.

Groovig

kotlinOptions {
    freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
                    project.absolutePath + "/compose_compiler_config.conf"
    ]
}

Kotlin

kotlinOptions {
  freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
  )
}

Da der Compose-Compiler für jedes Modul in Ihrem Projekt separat ausgeführt wird, können Sie bei Bedarf unterschiedliche Konfigurationen für verschiedene Module bereitstellen. Alternativ können Sie eine Konfiguration auf der Stammebene Ihres Projekts einrichten und diesen Pfad an jedes Modul übergeben.

Mehrere Module

Ein weiteres häufiges Problem betrifft die Multi-Modul-Architektur. Der Compiler Compose kann nur dann ableiten, ob eine Klasse stabil ist, wenn alle nicht-primitiven Typen, auf die er verweist, entweder explizit als stabil gekennzeichnet oder in einem Modul enthalten ist, das ebenfalls mit dem Compose-Compiler erstellt wurde.

Dieses Problem kann auftreten, wenn sich die Datenschicht in einem anderen Modul als die UI-Ebene befindet, was der empfohlene Ansatz ist.

Die Lösung

Sie haben folgende Möglichkeiten, das Problem zu lösen:

  1. Fügen Sie die Klassen der Compiler-Konfigurationsdatei hinzu.
  2. Aktivieren Sie den Compose-Compiler für Ihre Datenschichtmodule oder kennzeichnen Sie Ihre Klassen gegebenenfalls mit @Stable oder @Immutable.
    • Dazu müssen Sie der Datenschicht eine Abhängigkeit vom Typ „Compose“ hinzufügen. Es handelt sich jedoch nur um die Abhängigkeit für die Compose-Laufzeit und nicht für Compose-UI.
  3. Innerhalb Ihres UI-Moduls verpacken Sie Ihre Datenschichtklassen in UI-spezifische Wrapper-Klassen.

Das gleiche Problem tritt auch auf, wenn externe Bibliotheken verwendet werden, die nicht den Composer-Compiler verwenden.

Nicht jede zusammensetzbare Funktion sollte überspringbar sein

Wenn du Stabilitätsprobleme beheben möchtest, solltest du nicht versuchen, jede zusammensetzbare Funktion überspringbar zu machen. Dies kann zu einer vorzeitigen Optimierung führen, die mehr Probleme mit sich bringt, als behoben werden kann.

Es gibt viele Situationen, in denen die Möglichkeit, überspringbar zu sein, keinen echten Vorteil bietet und den Code möglicherweise nur schwer verwalten kann. Beispiele:

  • Eine zusammensetzbare Funktion, die nur selten oder überhaupt neu zusammengesetzt wird.
  • Eine zusammensetzbare Funktion, die selbst nur „überspringbare zusammensetzbare Funktionen“ bezeichnet.
  • Eine zusammensetzbare Funktion mit einer großen Anzahl von Parametern und teuren Equal-Implementierungen. In diesem Fall können die Kosten für die Überprüfung, ob sich ein Parameter geändert hat, die Kosten für eine kostengünstige Neuzusammensetzung überwiegen.

Wenn eine zusammensetzbare Funktion überspringbar ist, entsteht dadurch ein geringer Aufwand, der sich möglicherweise nicht lohnt. Sie können die zusammensetzbare Funktion sogar als nicht neustartbar kennzeichnen, wenn Sie feststellen, dass die Funktion für einen Neustart mehr Aufwand verursacht, als es wert ist.