Rozwiąż problemy ze stabilnością

Jeśli masz niestabilną klasę, która powoduje problemy z wydajnością, musisz ją ustabilizować. W tym dokumencie opisujemy kilka technik, które możesz w tym celu zastosować.

Włącz silne pomijanie

Najpierw spróbuj włączyć tryb szybkiego pomijania. Tryb silnego pomijania umożliwia pomijanie funkcji kompozycyjnych z niestabilnymi parametrami i jest najprostszym sposobem na rozwiązanie problemów z wydajnością spowodowanych niestabilnością.

Więcej informacji znajdziesz w sekcji Szybkie pomijanie.

Ustawianie klasy jako niezmiennej

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

  • Niezmienny: wskazuje typ, w którym wartość żadnych właściwości nie może się zmienić po utworzeniu instancji tego typu, a wszystkie metody są referencyjnie przejrzyste.
    • Upewnij się, że wszystkie właściwości klasy mają wartość val, a nie var, i że są to typy niezmienne.
    • Typy proste, takie jak String, IntFloat, są zawsze niezmienne.
    • Jeśli to niemożliwe, musisz użyć stanu Compose w przypadku wszystkich właściwości, które można zmieniać.
  • Stable: wskazuje typ, który można zmieniać. Środowisko wykonawcze Compose nie wie, czy i kiedy publiczne właściwości lub metody typu mogą dać inne wyniki niż poprzednie wywołanie.

Kolekcje niezmienne

Częstą przyczyną, dla której Compose uznaje klasę za niestabilną, są kolekcje. Jak wspomnieliśmy na stronie Diagnozowanie problemów ze stabilnością, kompilator Compose nie może mieć całkowitej pewności, że kolekcje takie jak List, MapSet są rzeczywiście niezmienne, dlatego oznacza je jako niestabilne.

Aby rozwiązać ten problem, możesz użyć kolekcji niezmiennych. Komponent Compose Compiler obsługuje Kotlinx Immutable Collections. Te kolekcje są niezmienne, a kompilator Compose traktuje je w ten sposób. Ta biblioteka jest nadal w wersji alfa, więc jej interfejs API może ulec zmianie.

Rozważmy ponownie tę niestabilną klasę z przewodnika Diagnostyka problemów ze stabilnością:

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

Możesz sprawić, że tags będzie stabilny, 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 stają się 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 do niestabilnych klas adnotacji @Stable lub @Immutable.

Adnotacja klasy zastępuje to, co kompilator wywnioskowałby na temat klasy. Jest podobny do !! operatora w języku Kotlin. Musisz bardzo uważać na to, jak używasz tych adnotacji. Zastąpienie działania kompilatora może prowadzić do nieprzewidzianych błędów, np. do sytuacji, w której funkcja kompozycyjna nie będzie ponownie komponowana, gdy tego oczekujesz.

Jeśli możesz ustabilizować klasę bez adnotacji, postaraj się to zrobić.

Poniższy fragment zawiera krótki przykład klasy danych z adnotacją immutable:

@Immutable
data class Snack(

)

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

Oznaczone klasy w kolekcjach

Rozważ 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 do adnotacji Snack adnotację @Immutable, kompilator Compose nadal oznaczy parametr snacksHighlightedSnacks jako niestabilny.

W przypadku typów kolekcji parametry mają ten sam problem co klasy: kompilator Compose zawsze oznacza parametr typu List jako niestabilny, nawet jeśli jest to kolekcja typów stabilnych.

Nie możesz oznaczyć poszczególnych parametrów jako stabilnych ani dodać adnotacji do funkcji kompozycyjnej, aby zawsze można było ją pominąć. Istnieje wiele ścieżek rozwoju.

Problem z niestabilnymi kolekcjami możesz rozwiązać na kilka sposobów. W kolejnych podsekcjach opisujemy te różne podejścia.

Plik konfiguracji

Jeśli zgadzasz się na przestrzeganie umowy dotyczącej stabilności w swojej bazie kodu, możesz wyrazić zgodę na uznawanie kolekcji w języku Kotlin za stabilne, dodając kotlin.collections.* do pliku konfiguracji stabilności.

Kolekcja niezmienna

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

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

Wrapper

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

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

Możesz go następnie 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 jednej z tych metod kompilator Compose oznaczy funkcję kompozycyjną 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 Compose może teraz pominąć HighlightedSnacks, jeśli żadne z jego danych wejściowych nie uległy zmianie.

Plik konfiguracji stabilności

Od wersji 1.5.5 kompilatora Compose w czasie kompilacji można podać plik konfiguracyjny klas, które mają być uznawane za stabilne. Pozwala to uznać za stabilne klasy, nad którymi nie masz kontroli, np. klasy biblioteki standardowej, takie jak LocalDateTime.

Plik konfiguracji to zwykły plik tekstowy, w którym każdy wiersz zawiera jedną klasę. Obsługiwane są komentarze, 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ę, przekaż ścieżkę pliku konfiguracyjnego do bloku opcji composeCompiler w konfiguracji wtyczki Gradle kompilatora Compose.

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

Kompilator Compose działa na każdym module w projekcie osobno, więc w razie potrzeby możesz podać różne konfiguracje dla różnych modułów. Możesz też mieć jedną konfigurację na poziomie głównym projektu i przekazywać tę ścieżkę do każdego modułu.

Wiele modułów

Inny typowy problem dotyczy architektury wielomodułowej. Kompilator Compose może określić, czy klasa jest stabilna, tylko wtedy, gdy wszystkie typy inne niż proste, do których się odwołuje, są wyraźnie oznaczone jako stabilne lub znajdują się w module, który również został skompilowany za pomocą kompilatora Compose.

Jeśli warstwa danych znajduje się w osobnym module niż warstwa interfejsu, co jest zalecanym podejściem, może to być problem, który napotkasz.

Rozwiązanie

Aby rozwiązać ten problem, możesz wykonać jedną z tych czynności:

  1. Dodaj klasy do pliku konfiguracji kompilatora.
  2. Włącz kompilator Compose w modułach warstwy danych lub oznacz klasy adnotacjami @Stable lub @Immutable w odpowiednich miejscach.
    • Wymaga to dodania zależności Compose do warstwy danych. Jest to jednak tylko zależność od środowiska wykonawczego Compose, a nie od Compose-UI.
  3. W module interfejsu umieść klasy warstwy danych w klasach opakowujących specyficznych dla interfejsu.

Ten sam problem występuje też w przypadku bibliotek zewnętrznych, jeśli nie korzystają one z kompilatora Compose.

Nie każdy element kompozycyjny powinien być pomijalny

Podczas rozwiązywania problemów ze stabilnością nie próbuj sprawić, aby każdy komponent był pomijalny. Próba optymalizacji na tym etapie może prowadzić do przedwczesnej optymalizacji, która wprowadza więcej problemów niż rozwiązuje.

W wielu sytuacjach możliwość pominięcia nie przynosi żadnych korzyści i może prowadzić do trudnego w utrzymaniu kodu. Na przykład:

  • Funkcja kompozycyjna, która nie jest często lub wcale ponownie komponowana.
  • Funkcja kompozycyjna, która sama wywołuje tylko funkcje kompozycyjne możliwe do pominięcia.
  • Funkcja kompozycyjna z dużą liczbą parametrów z kosztownymi implementacjami equals. W takim przypadku koszt sprawdzenia, czy jakikolwiek parametr uległ zmianie, może przewyższać koszt taniego ponownego komponowania.

Gdy funkcja kompozycyjna jest pomijalna, wiąże się to z niewielkim narzutem, który może nie być tego wart. Możesz nawet dodać adnotację do funkcji kompozycyjnej, aby była nieuruchamiana ponownie w przypadkach, gdy uznasz, że możliwość ponownego uruchomienia jest bardziej obciążająca niż warta.