Rozwiąż problemy ze stabilnością

Jeśli klasa nie jest stabilna, która powoduje problemy z wydajnością, ustaw ją jako stabilną. Opisujemy w nim różne techniki, które możesz wykorzystać.

Włączanie pomijania

Najpierw spróbuj włączyć tryb pomijania. Tryb pomijania z wysoką czułością umożliwia pomijanie komponentów z niestabilnymi parametrami i jest najłatwiejszą metodą rozwiązywania problemów z wydajnością spowodowanych niestabilnością.

Więcej informacji znajdziesz w sekcji Pomijanie treści.

Ustaw klasę jako niezmienną

Możesz też spróbować uczynić klasę niestabilną całkowicie.

  • Niezmienne: wskazuje typ, w którym wartość właściwości nigdy nie może się zmienić po utworzeniu instancji tego typu, a wszystkie metody są przezroczyste w sposób referencyjny.
    • Upewnij się, że wszystkie właściwości klasy mają typ val (a nie var) i są typu niezmiennego.
    • Typy proste, takie jak String, IntFloat, są zawsze niezmienne.
    • Jeśli nie jest to możliwe, musisz użyć stanu Compose w przypadku właściwości, które można zmieniać.
  • Stały: wskazuje typ, który można zmienić. Środowisko wykonawcze Compose nie zdaje sobie sprawy, czy któreś z właściwości publicznych lub metod danego typu zwróciłoby inne wyniki z poprzedniego wywołania.

Stałe kolekcje

Zbiory są częstą przyczyną, dla której Compose uznaje klasę za niestabilną. Jak wspomnieliśmy na stronie Diagnozuj problemy ze stabilnością, kompilator Compose nie ma całkowitej pewności, że kolekcje takie jak List, Map czy Set są prawdziwie stałe, dlatego oznacza je jako niestabilne.

Aby rozwiązać ten problem, możesz użyć kolekcji stałych. Kompilator Compose obsługuje niezmienną kolekcję Kotlinx. Te kolekcje są niezmienne i kompilator Compose tak je traktuje. Ta biblioteka jest nadal w wersji alfa, możesz się więc spodziewać zmian w interfejsie API.

Weź pod uwagę tę niestabilną klasę z przewodnika Diagnostyka problemów ze stabilnością:

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

Możesz ustabilizować tags, używając niezmiennej kolekcji. W klasie zmień typ tags na ImmutableSet<String>:

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

Po wykonaniu tej czynności wszystkie parametry klasy pozostają niezmienne, a kompilator Compose oznacza klasę jako stabilną.

Dodawanie adnotacji za pomocą Stable lub Immutable

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

Dodawanie adnotacji do klasy powoduje zastąpienie informacji, które kompilator wyciągnie na temat Twojej klasy. Jest podobny do operatora !! w Kotlin. Należy bardzo uważać na to, jak używasz tych adnotacji. Zastępowanie działania kompilatora może prowadzić do nieprzewidzianych błędów, takich jak funkcja kompozycyjna nie utworzona ponownie, gdy tego oczekujesz.

Jeśli można ustabilizować zajęcia bez adnotacji, postaraj się to zrobić.

Ten 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ą.

Zaznaczone zajęcia w kolekcjach

Rozważ funkcję kompozycyjną zawierającą parametr typu List<Snack>:

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

Nawet jeśli dodasz adnotację Snack z wartością @Immutable, kompilator usługi Compose nadal będzie oznaczać parametr snacks w elementach HighlightedSnacks jako niestabilny.

Jeśli chodzi o typy kolekcji, parametry mają taki sam problem jak klasy. Kompilator Compose zawsze oznacza parametr typu List jako niestabilny, nawet jeśli jest to zbiór typów stabilnych.

Nie możesz oznaczyć pojedynczego parametru jako stabilnego ani dodać adnotacji do składowej, aby była zawsze pomijalna. Istnieje wiele ścieżek do przodu.

Problem z niestabilnymi kolekcjami można rozwiązać na kilka sposobów. W poniższych sekcjach opisaliśmy różne podejścia.

Plik konfiguracji

Jeśli zgadzasz się na przestrzeganie umowy dotyczącej stabilności w bazie kodu, możesz uznać kolekcje Kotlin jako stabilne, dodając kotlin.collections.* do pliku konfiguracji stabilności.

Kolekcja stała

Aby zapewnić bezpieczeństwo niezmienności w czasie kompilacji, zamiast List możesz użyć niezmiennej kolekcji kotlinx.

@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ść obiekt List w klasie stabilnej z adnotacjami. W zależności od wymagań najlepszym rozwiązaniem może być ogólny element opakowujący.

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

Następnie możesz użyć go jako typu parametru w elemencie kompozycyjnym.

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

Rozwiązanie

Po zastosowaniu jednej z tych metod kompilator Compose oznacza teraz komponent HighlightedSnacks jako skippablerestartable.

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 ponownego komponowania funkcji Utwórz może teraz pomijać element HighlightedSnacks, jeśli nie zmieniły się żadne jego dane wejściowe.

Plik konfiguracji stabilności

Począwszy od kompilatora Compose 1.5.5 można podać plik konfiguracji z klasami, które mają być uważane za stabilne, w momencie kompilacji. Dzięki temu możesz uznać klasy, których nie kontrolujesz, takie jak standardowe klasy biblioteki, takie jak LocalDateTime, jako stabilne.

Plik konfiguracji jest zwykłym plikiem tekstowym z jedną klasą na wiersz. Obsługiwane są komentarze oraz pojedyncze i podwójne symbole wieloznaczne. Przykładowa konfiguracja:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// 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ę, prześlij ścieżkę do pliku konfiguracji do bloku opcji composeCompiler w konfiguracji wtyczki kompilatora Compose dla Gradle.

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

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

Wiele modułów

Innym częstym problemem jest architektura wielomodułowa. Kompilator Compose może stwierdzić, czy klasa jest stabilna, tylko wtedy, gdy wszystkie typy niepierwotne, do których się odwołuje, są wyraźnie oznaczone jako stabilne lub znajdują się w module, który został również skompilowany za pomocą kompilatora Compose.

Taki problem może występować, jeśli warstwa danych znajduje się w osobnym module niż warstwa interfejsu (zalecane).

Rozwiązanie

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

  1. Dodaj klasy do pliku konfiguracji kompilatora.
  2. Włącz kompilator Compose w modułach warstwy danych lub w odpowiednich przypadkach dodaj do klas tagi @Stable lub @Immutable.
    • Wymaga to dodania zależności tworzenia wiadomości do warstwy danych. Jest to jednak wymagane tylko w środowisku wykonawczym Compose, a nie środowisku Compose-UI.
  3. W module interfejsu użytkownika owiń klasy warstwy danych w klasy opakowujące przeznaczone do interfejsu użytkownika.

Ten sam problem występuje też podczas korzystania z bibliotek zewnętrznych, jeśli nie używają one kompilatora Compose.

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

Podczas rozwiązywania problemów ze stabilnością nie należy dążyć do tego, aby wszystkie elementy kompozycyjne były możliwe do pominięcia. Może to spowodować przedwczesną optymalizację, która spowoduje więcej problemów niż rozwiąże.

Jest wiele sytuacji, w których możliwość pominięcia nie przynosi żadnych korzyści, a może prowadzić do trudnego w utrzymaniu kodu. Na przykład:

  • Element kompozycyjny, który nie podlega ponownej kompozycji lub w ogóle nie jest skomponowany.
  • Elementy kompozycyjne, które same w sobie nazywane są elementami kompozycyjnymi możliwymi do pominięcia.
  • Element kompozycyjny z dużą liczbą parametrów i drogich równa się implementacji. W takiej sytuacji koszt sprawdzenia, czy którykolwiek parametr się nie zmienił, może przewyższyć koszt taniej zmiany kompozycji.

Gdy element kompozycyjny jest możliwy do pominięcia, wiąże się z niewielkim narzutem, który może nie być wart. Możesz nawet dodać adnotację do komponentu, aby był niemożliwy do ponownego uruchomienia w przypadku, gdy uznasz, że ponowne uruchamianie jest nieopłacalne.