Rozwiąż problemy ze stabilnością

Jeśli masz niestabilną klasę, która powoduje problemy z wydajnością, powinna być stabilna. W tym dokumencie opisujemy kilka technik, które można wykorzystać.

Ustawianie klasy jako stałej

Zacznij od przekształcenia klasy niestabilnej w całkowicie stałe.

  • Stała: wskazuje typ, w którym wartość właściwości nigdy nie może się zmienić po utworzeniu wystąpienia danego typu, a wszystkie metody są zazwyczaj przejrzyste.
    • Upewnij się, że wszystkie właściwości klasy to val, a nie var, i typów stałych.
    • Typy podstawowe, takie jak String, Int czy Float, są zawsze stałe.
    • Jeśli jest to niemożliwe, w przypadku wszystkich zmiennych właściwości musisz używać stanu tworzenia.
  • Stabilny: oznacza typ zmienny. Środowisko wykonawcze tworzenia nie informuje o tym, czy i kiedy dowolna z publicznych właściwości lub metod działania danego typu dałaby inne wyniki niż w poprzednim wywołaniu.

Kolekcje stałe

Częstą przyczyną, dla której funkcja Compose uważa, że klasa jest niestabilna, są kolekcje. Jak wspomnieliśmy na stronie Diagnozuj problemy ze stabilnością, kompilator Compose nie może mieć całkowitej pewności, że kolekcje takie jak List, Map i Set są naprawdę stałe, dlatego są oznaczane jako niestabilne.

Aby rozwiązać ten problem, możesz użyć kolekcji stałych. Kompilator Compose obsługuje kolekcje stałe Kotlinx. Te kolekcje na pewno nie będą się zmieniać, a kompilator Compose traktuje je tak. Ta biblioteka jest nadal w wersji alfa, więc możesz się spodziewać zmian w jej interfejsie API.

Przyjrzyj się jeszcze tej niestabilnej klasie z przewodnika Diagnozuj problemy ze stabilnością:

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

Możesz udostępnić usługę tags stabilną za pomocą kolekcji stałej. W klasie zmień typ tags na ImmutableSet<String>:

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

Po wykonaniu tej czynności wszystkie parametry klasy są stałe, a kompilator Compose oznacza klasę jako stabilną.

Dodaj adnotację Stable lub Immutable

Możliwym sposobem rozwiązania problemów ze stabilnością jest dodanie adnotacji do niestabilnych klas za pomocą właściwości @Stable lub @Immutable.

Dodanie adnotacji do klasy zastępuje informacje, które kompilator wywnioskował na temat klasy. Jest on podobny do operatora !! w Kotlin. Ostrożnie korzystaj z tych adnotacji. Zastąpienie działania kompilatora może prowadzić do nieprzewidzianych błędów, na przykład funkcji kompozycyjnych, które nie powinny zostać utworzone w oczekiwany sposób.

Jeśli możesz, by klasa była stabilna bez adnotacji, postaraj się w ten sposób uzyskać stabilność.

Poniższy fragment kodu zawiera minimalny przykład klasy danych oznaczonej jako niezmienna:

@Immutable
data class Snack(
…
)

Niezależnie od tego, czy używasz adnotacji @Immutable czy @Stable, kompilator Compose oznacza klasę Snack jako stabilną.

Klasy z adnotacjami w kolekcjach

Zastanów się nad funkcją kompozycyjną, która zawiera parametr typu List<Snack>:

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

Nawet jeśli dodasz adnotacje do Snack za pomocą @Immutable, kompilator Compose nadal oznacza parametr snacks w HighlightedSnacks jako niestabilny.

W przypadku parametrów występuje ten sam problem co klasy w przypadku typów kolekcji. Kompilator Compose zawsze oznacza parametr typu List jako niestabilny, nawet jeśli jest to zbiór typów stabilnych.

Nie możesz oznaczyć pojedynczych parametrów jako stałych ani dodawać adnotacji do elementu kompozycyjnego, by zawsze był możliwa do pominięcia. Istnieje kilka ścieżek do przodu.

Problem z niestabilnymi kolekcjami możesz rozwiązać na kilka sposobów. W poniższych sekcjach opisujemy poszczególne podejścia.

Plik konfiguracji

Jeśli chcesz zachować zgodność z umową dotyczącą stabilności w swojej bazie kodu, możesz uznać kolekcje Kotlin jako stabilne, dodając kotlin.collections.* do pliku konfiguracji stabilności.

Stała kolekcja

Aby kompilować bezpieczeństwo czasowe i niezmienność, możesz użyć stałej kolekcji kotlinx zamiast List.

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

Wrapper

Jeśli nie możesz użyć kolekcji stałej, możesz utworzyć własną. Aby to zrobić, umieść List w stabilnej klasie z adnotacjami. W zależności od Twoich wymagań najlepszym wyborem będzie prawdopodobnie ogólny kod.

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

Możesz go potem użyć jako typu parametru w funkcji kompozycyjnej.

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

Rozwiązanie

Po zastosowaniu jednego z tych rozwiązań kompilator funkcji Compose oznacza teraz kompozycję HighlightedSnacks jako zarówno skippable, jak i 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
)

Podczas zmiany kompozycji funkcja tworzenia może teraz pominąć element HighlightedSnacks, jeśli żadne dane wejściowe nie zostaną zmienione.

Plik konfiguracji stabilności

Począwszy od wersji 1.5.5 kompilatora tworzenia wiadomości podczas kompilacji można udostępnić plik konfiguracji klas uznawanych za stabilne. Dzięki temu zajęcia, nad którymi nie masz kontroli, takie jak standardowe klasy biblioteki, takie jak LocalDateTime, mogą zostać uznane za stabilne.

Plik konfiguracji to zwykły plik tekstowy z 1 klasą w każdym wierszu. Obsługiwane są komentarze oraz symbole wieloznaczne pojedyncze i podwójne. Oto przykładowa konfiguracja:

// 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<*,_>

Aby włączyć tę funkcję, przekaż ścieżkę pliku konfiguracji do opcji kompilatora w usłudze Compose.

Odlotowy

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"
  )
}

Kompilator Compose działa w każdym module w projekcie oddzielnie, dlatego w razie potrzeby możesz udostępnić różne konfiguracje różnym modułom. Możesz też dodać jedną konfigurację na poziomie głównym projektu i przekazać tę ścieżkę do każdego modułu.

Wiele modułów

Kolejny typowy problem to architektura składająca się z wielu modułów. Kompilator Compose może określić, czy klasa jest stabilna tylko wtedy, gdy wszystkie typy inne niż podstawowe, do których się odwołuje, są wyraźnie oznaczone jako stabilne lub znajdują się w module, który został również utworzony za pomocą kompilatora Compose.

Jeśli warstwa danych znajduje się w osobnym module względem warstwy interfejsu, a jest to zalecane, może występować ten problem.

Rozwiązanie

Aby rozwiązać ten problem, wykonaj jedną z tych czynności:

  1. Dodaj klasy do pliku konfiguracji kompilacji.
  2. Włącz kompilator Compose w modułach warstwy danych lub w odpowiednich przypadkach dodaj do klas tag @Stable bądź @Immutable.
    • Wymaga to dodania do warstwy danych zależności tworzenia. Jest to jednak zależność wyłącznie od środowiska wykonawczego tworzenia wiadomości, a nie od Compose-UI.
  3. W module interfejsu użytkownika dodaj klasy warstwy danych do klas opakowań specyficznych dla UI.

Ten sam problem występuje też w przypadku korzystania z bibliotek zewnętrznych, które nie używają kompilatora Compose.

Nie wszystkie funkcje kompozycyjne muszą być możliwe do pominięcia

Przy rozwiązywaniu problemów ze stabilnością nie próbuj wprowadzać wszystkich funkcji kompozycyjnych do pominięcia. Próba uzyskania dostępu do tych danych może prowadzić do przedwczesnej optymalizacji, która spowoduje więcej problemów niż naprawienie.

Jest wiele sytuacji, w których możliwość pominięcia nie daje żadnych korzyści i może utrudniać utrzymanie kodu. Na przykład:

  • Funkcja kompozycyjna, która nie jest ponownie tworzona często lub w ogóle.
  • Funkcja kompozycyjna, która sama wywołuje możliwe do pominięcia elementy kompozycyjne.
  • Funkcja kompozycyjna o dużej liczbie parametrów i drogimi implementacjami równa się. W tym przypadku koszt sprawdzenia, czy jakiś parametr uległ zmianie, może przeważyć koszt taniej rekompozycji.

Możliwość pominięcia funkcji kompozycyjnej wiąże się z pewnym narzutem, który może być nieopłacalny. Możesz nawet dodać adnotacje do elementu kompozycyjnego, aby nie można go było ponownie uruchomić, jeśli stwierdzisz, że ponowne uruchomienie wymaga większych nakładów niż jest tego warte.