Jeśli masz do czynienia z niestabilną klasą, która powoduje problemy z wydajnością, zoptymalizuj sposób, w jaki Compose ją porównuje lub tworzy jej instancje. W tym dokumencie opisujemy kilka technik, które możesz zastosować.
Włączanie silnego pomijania
Tryb silnego pomijania jest domyślnie włączony od wersji Kotlin 2.0.20.
Tryb silnego pomijania umożliwia pomijanie funkcji kompozycyjnych z niestabilnymi parametrami przez porównywanie niestabilnych parametrów przy użyciu równości instancji (===), podczas gdy stabilne parametry są nadal porównywane przy użyciu równości strukturalnej (Object.equals()).
Jeśli używasz wersji Kotlina starszej niż 2.0.20, najpierw włącz tryb silnego pomijania. Jeśli tryb silnego pomijania jest już włączony, a funkcja kompozycyjna nadal niepotrzebnie wykonuje rekompozycję, sprawdź, czy nowe instancje niestabilnych parametrów są przydzielane przy każdym przejściu rekompozycji.
Więcej informacji znajdziesz w sekcji Silne pomijanie.
Ustaw klasę jako niezmienną
Możesz też ustawić niestabilną klasę jako całkowicie niezmienną, aby kompilator mógł automatycznie wnioskować o jej stabilności.
- Niezmienny: wskazuje typ, w którym wartość żadnej właściwości nie może się zmienić po utworzeniu instancji tego typu, a wszystkie metody są przezroczyste referencyjnie.
- Upewnij się, że wszystkie właściwości klasy są
val, a nievar, i mają niezmienne typy. - Typy podstawowe, takie jak
String,IntiFloat, są zawsze niezmienne. - Płaskie typy
data classz właściwościami podstawowymivallubStringw modułach utworzonych za pomocą kompilatora Compose są już wnioskowane jako stabilne i automatycznie porównywane z.equals()– nie musisz dodawać do nich adnotacji@Immutable. - Jeśli niezmienność jest niemożliwa, musisz użyć stanu Compose w przypadku wszystkich właściwości zmiennych.
- Upewnij się, że wszystkie właściwości klasy są
- Stable: wskazuje typ, który jest zmienny. Środowisko wykonawcze Compose nie dowiaduje się, czy i kiedy właściwości publiczne lub zachowanie metody danego typu dadzą inne wyniki niż poprzednie wywołanie.
Kolekcje i stabilność
Częstą przyczyną, dla której Compose uważa klasę za niestabilną, są kolekcje. Jak wspomniano na stronie Diagnozowanie problemów ze stabilnością, kompilator Compose nie może mieć całkowitej pewności, że interfejsy kolekcji, takie jak List, Map i Set, są naprawdę niezmienne, dlatego oznacza je jako niestabilne.
Rozważ tę klasę z przewodnika Diagnozowanie problemów ze stabilnością:
unstable class Snack {
…
unstable val tags: Set<String>
…
}
Gdy włączony jest tryb pomijania (domyślny w Kotlin 2.0.20 i nowszych), niestabilne kolekcje i klasy zawierające kolekcje pomijają już rekompozycję, używając równości instancji (===), gdy przekazywane jest to samo odwołanie.
Zachowaj ostrożność, zanim zastąpisz kotlin.collections.List, Set lub Map kolekcjami Kotlinx Immutable Collections (ImmutableList, ImmutableSet, ImmutableMap) lub dodasz reguły @Immutable, @Stable lub stability_config.conf do modeli zawierających kolekcje. W trybie silnego pomijania standardowe List pomija przy użyciu równości referencyjnej (===), podczas gdy ImmutableList i modele kolekcji z adnotacjami przeprowadzają porównania .equals() element po elemencie przy każdym przebiegu rekompozycji.
W przypadku dużych kolekcji lub układów typu lazy (LazyColumn, LazyRow, LazyGrid) sprawdzanie .equals() każdego elementu może potrwać dłużej niż rekompozycja. Ten koszt podwaja się, jeśli przekazujesz właściwość ImmutableList z data class do funkcji kompozycyjnej podrzędnej:
data class SnackCategory(
val name: String,
val snacks: ImmutableList<Snack>
)
@Composable
fun CategorySection(category: SnackCategory) {
Text(category.name)
// Compose compares category.snacks twice: once in SnackCategory.equals()
// for CategorySection, and again for HighlightedSnacks.
HighlightedSnacks(snacks = category.snacks)
}
Zamiast wymuszać równość strukturalną kolekcji, zachowaj odwołanie do instancji kolekcji w różnych emisjach (np. przez buforowanie przekształceń w ViewModel lub za pomocą remember), aby Compose mogło pominąć używanie równości instancji (===). Rozważ użycie adnotacji ImmutableList lub adnotacji stabilności w modelach przechowujących kolekcje tylko wtedy, gdy nie możesz zachować odwołania do instancji kolekcji, a testy porównawcze potwierdzają, że kontrole równości strukturalnej są szybsze niż rekompozycja.
Jeśli używasz wersji Kotlina starszej niż 2.0.20, najpierw włącz tryb silnego pomijania.
Dodawanie adnotacji za pomocą Stable lub Immutable
Możliwym sposobem na rozwiązanie problemów ze stabilnością w przypadku małych klas z niedrogimi kontrolami .equals() jest dodanie do niestabilnych klas adnotacji @Stable lub @Immutable.
Adnotacja klasy zastępuje to, co kompilator w innym przypadku wywnioskowałby o klasie. Jest on podobny do operatora !! w języku Kotlin. Należy bardzo ostrożnie korzystać z tych adnotacji. Zastąpienie zachowania kompilatora może prowadzić do nieoczekiwanych błędów, takich jak brak rekompozycji funkcji kompozycyjnej, gdy tego oczekujesz.
Jeśli włączony jest tryb silnego pomijania, podczas podejmowania decyzji o dodaniu @Stable lub @Immutable postępuj zgodnie z tymi wytycznymi:
- Nie dodawaj adnotacji do klas, które są już uznawane za stabilne: klasy z właściwościami
valstabilnych typów w modułach utworzonych za pomocą kompilatora Compose są już uznawane za stabilne. - Dodawaj adnotacje do klas tylko z prostymi testami
.equals(), gdy instancje są tworzone ponownie: dodaj@Stablelub@Immutable, gdy kompilator nie może wywnioskować stabilności (np. interfejsy lub klasy z modułów innych niż Compose) oraz źródło danych przydziela nowe instancje obiektów dla niezmienionych elementów przy każdej emisji, pod warunkiem że klasa jest mała i ma prosty test.equals(). - Zachowaj ostrożność w przypadku modeli zawierających kolekcje: unikaj dodawania adnotacji
@Stablelub@Immutabledo modeli danych, które zawierają duże kolekcje (List,Set,Map), zwłaszcza w przypadku modeli używanych w układach leniwych (LazyColumn,LazyRow,LazyGrid,Pager). Pozostawienie tych modeli bez adnotacji pozwala Compose porównywać je za pomocą równości instancji (===) zamiast przeprowadzania kontroli strukturalnej.equals()w przypadku każdego elementu.
Poniższy fragment kodu zawiera minimalny przykład płaskiej klasy danych (bez właściwości kolekcji) oznaczonej jako niezmienna, gdy jest zdefiniowana w module, w którym kompilator nie może wywnioskować stabilności:
@Immutable
data class SnackSummary(
val id: Long,
val name: String,
val price: Long
)
Niezależnie od tego, czy używasz adnotacji @Immutable czy @Stable, kompilator Compose oznacza klasę SnackSummary jako stabilną i porównuje ją za pomocą Object.equals().
Zajęcia z adnotacjami w kolekcjach
Rozważ funkcję kompozycyjną, która zawiera parametr typu List<Snack>:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
unstable snacks: List<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
Nawet jeśli Snack jest stabilny lub oznaczony adnotacją @Immutable, kompilator Compose nadal oznacza parametr snacks w HighlightedSnacks jako unstable, ponieważ List jest interfejsem.
Gdy włączony jest tryb silnego pomijania, funkcja HighlightedSnacks jest nadal oznaczona jako skippable i pomija rekompozycję, używając równości instancji (===), gdy przekazywana jest ta sama instancja List. Możesz ustawić parametr snacks jako stable, opakowując List<Snack> w klasę opakowującą @Immutable, zastępując List<Snack> przez ImmutableList<Snack> lub dodając kotlin.collections.* do pliku konfiguracji stabilności.
Jednak w takim przypadku Compose przeprowadza porównanie element po elemencie .equals()
przy każdym przejściu rekompozycji. Używaj tych metod tylko wtedy, gdy nie możesz zachować odwołania do instancji listy, a testy porównawcze wykazują korzyści.
Plik konfiguracji stabilności
Począwszy od kompilatora Compose w wersji 1.5.5, w czasie kompilacji można podać plik konfiguracji klas, które mają być uznawane za stabilne. Dzięki temu klasy, których nie kontrolujesz, np. klasy biblioteki standardowej, takie jak LocalDateTime, mogą być uznawane za stabilne.
Plik konfiguracji to zwykły plik tekstowy z jedną klasą w każdym wierszu. 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ę, przekaż ścieżkę pliku konfiguracji do bloku opcji composeCompiler w konfiguracji wtyczki Gradle kompilatora Compose.
composeCompiler {
stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}
Kompilator Compose działa osobno w każdym module projektu, 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 częsty problem dotyczy architektury wielomodułowej. Kompilator Compose może wywnioskować, czy klasa jest stabilna, tylko wtedy, gdy wszystkie typy nieprymitywne, do których się odwołuje, są wyraźnie oznaczone jako stabilne lub znajdują się w module, który również został utworzony za pomocą kompilatora Compose.
Jeśli warstwa danych znajduje się w osobnym module niż warstwa interfejsu (co jest zalecanym podejściem), możesz napotkać ten problem. W trybie silnego pomijania klasy z modułów innych niż Compose są traktowane jako niestabilne i nadal pomijają rekompozycję przy użyciu równości instancji (===), gdy przekazywana jest ta sama instancja. Jeśli jednak warstwa danych często przydziela nowe instancje małych, płaskich modeli o identycznych wartościach przy każdej emisji, możesz zdecydować się na równość strukturalną (Object.equals()).
Rozwiązanie
Aby rozwiązać ten problem, możesz wykonać jedną z tych czynności:
- Dodaj małe, płaskie klasy z tanimi testami
.equals()do pliku konfiguracji kompilatora. - Włącz kompilator Compose w modułach warstwy danych lub oznacz klasy tagami
@Stablelub@Immutable, jeśli jest to konieczne.- Wymaga to dodania zależności Compose do warstwy danych. Jest to jednak tylko zależność środowiska wykonawczego Compose, a nie
Compose-UI.
- Wymaga to dodania zależności Compose do warstwy danych. Jest to jednak tylko zależność środowiska wykonawczego Compose, a nie
- W module interfejsu opakuj klasy warstwy danych w klasy opakowujące przeznaczone dla interfejsu.
Ten sam problem występuje też w przypadku bibliotek zewnętrznych, jeśli nie używają kompilatora Compose.
Nie każdy element kompozycyjny powinien być możliwy do pominięcia
Podczas rozwiązywania problemów ze stabilnością nie próbuj wymuszać, aby każdy element kompozycyjny był możliwy do pominięcia, ani nie wymuszaj równości strukturalnej każdego parametru. Pominięcie funkcji kompozycyjnej nie jest bezpłatne: każdy parametr funkcji kompozycyjnej, którą można pominąć, jest przechowywany przez środowisko wykonawcze Compose tak, jakby został zapamiętany. Compose musi porównać każdy parametr z jego poprzednią wartością za każdym razem, gdy funkcja jest rekomponowana, aby określić, czy należy ją pominąć. Próba wymuszenia pomijania wszędzie może prowadzić do przedwczesnej optymalizacji, która wprowadza więcej narzutu, niż pozwala zaoszczędzić.
W wielu sytuacjach wymuszanie stabilności strukturalnej nie przynosi żadnych realnych korzyści i może pogorszyć wydajność lub łatwość konserwacji. Przykład:
- funkcja kompozycyjna, która nie jest często rekomponowana lub nie jest rekomponowana wcale;
- Funkcja kompozycyjna, która sama w sobie wywołuje tylko funkcje kompozycyjne możliwe do pominięcia.
- funkcja kompozycyjna z dużą liczbą parametrów lub modele kolekcji z kosztownymi implementacjami
.equals(). W takim przypadku koszt sprawdzenia, czy któryś z parametrów się zmienił, może przewyższyć koszt taniej rekompozycji.
Nie skupiaj się wyłącznie na liczbie rekompozycji. Skup się na tworzeniu lekkich funkcji kompozycyjnych, odraczaniu odczytów szybko zmieniającego się stanu do faz układu lub rysowania i weryfikowaniu zmian za pomocą testów porównawczych. Gdy funkcja kompozycyjna jest możliwa do pominięcia, dodaje ona niewielki narzut, który może nie być tego warty. Możesz nawet dodać do funkcji kompozycyjnej adnotację, że nie można jej ponownie uruchomić lub niemożliwy do pominięcia, jeśli uznasz, że możliwość ponownego uruchomienia lub pominięcia jest zbyt obciążająca.