Jetpack Compose to nowoczesny deklaratywny zestaw narzędzi interfejsu na Androida. Compose upraszcza pisanie i utrzymywanie interfejsu aplikacji, ponieważ udostępnia deklaratywny interfejs API, który umożliwia renderowanie interfejsu aplikacji bez konieczności imperatywnego zmieniania widoków interfejsu. Ta terminologia wymaga wyjaśnienia, ale jej konsekwencje są ważne dla projektu aplikacji.
Paradygmat programowania deklaratywnego
Tradycyjnie hierarchia widoków Androida była reprezentowana jako drzewo widżetów interfejsu. Gdy stan aplikacji zmienia się w wyniku interakcji użytkownika, hierarchia interfejsu musi zostać zaktualizowana, aby wyświetlać bieżące dane.
Najczęstszym sposobem aktualizowania interfejsu jest przechodzenie przez drzewo za pomocą funkcji takich jak findViewById() i zmienianie węzłów przez wywoływanie metod takich jak button.setText(String), container.addChild(View) lub img.setImageBitmap(Bitmap). Te metody
zmieniają stan wewnętrzny widżetu.
Ręczne manipulowanie widokami zwiększa prawdopodobieństwo błędów. Jeśli fragment danych jest renderowany w wielu miejscach, możesz zapomnieć o zaktualizowaniu jednego z widoków, w których się pojawia. Może to również prowadzić do nieprawidłowych stanów, gdy 2 aktualizacje w nieoczekiwany sposób powodują konflikt. Na przykład aktualizacja może próbować ustawić wartość węzła, który został właśnie usunięty z interfejsu. Ogólnie złożoność konserwacji oprogramowania rośnie wraz z liczbą widoków, które wymagają aktualizacji.
W ciągu ostatnich kilku lat cała branża zaczęła przechodzić na deklaratywny model interfejsu, który upraszcza tworzenie i aktualizowanie interfejsów użytkownika.
Ta technika polega na koncepcyjnym ponownym generowaniu całego ekranu od zera, a następnie wprowadzaniu tylko niezbędnych zmian. Dzięki temu unikniesz złożoności ręcznego aktualizowania hierarchii widoków stanowych. Compose to deklaratywna platforma interfejsu.
Jednym z problemów związanych z odświeżaniem całego ekranu jest to, że może być ono kosztowne pod względem czasu, mocy obliczeniowej i zużycia baterii. Aby zmniejszyć ten koszt, Compose inteligentnie wybiera, które części interfejsu użytkownika należy ponownie narysować w danym momencie. Ma to pewien wpływ na sposób projektowania komponentów interfejsu, o czym piszemy w sekcji Rekompozycja.
Przykładowa funkcja typu „composable"
Za pomocą Compose możesz tworzyć interfejs użytkownika, definiując zestaw funkcji kompozycyjnych, które przyjmują dane i emitują elementy interfejsu. Przykładem jest Greetingwidżet, który przyjmuje Stringi emituje Textwidżet wyświetlający wiadomość powitalną.
Kilka ważnych informacji o tej funkcji:
- Adnotacja: funkcja jest opatrzona adnotacją
@Composable. Wszystkie funkcje kompozycyjne muszą mieć tę adnotację. Ta adnotacja informuje kompilator Compose, że ta funkcja ma przekształcać dane w interfejs. - Dane wejściowe: funkcja przyjmuje dane. Funkcje kompozycyjne mogą przyjmować parametry, które umożliwiają logice aplikacji opisywanie interfejsu. W tym przypadku nasz widżet akceptuje
String, dzięki czemu może powitać użytkownika po imieniu. - Wyświetlanie w interfejsie: funkcja wyświetla tekst w interfejsie. W tym celu wywołuje funkcję typu „composable”
Text(), która tworzy element interfejsu tekstu. Funkcje typu „composable” emitują hierarchię interfejsu, wywołując inne funkcje typu „composable”. - Brak wartości zwracanej: funkcja niczego nie zwraca. Funkcje kompozycyjne, które emitują interfejs, nie muszą niczego zwracać, ponieważ opisują stan ekranu docelowego zamiast tworzyć widżety interfejsu.
Właściwości: ta funkcja jest szybka, idempotentna i nie ma efektów ubocznych.
- Funkcja działa tak samo, gdy jest wywoływana wielokrotnie z tym samym argumentem, i nie używa innych wartości, takich jak zmienne globalne czy wywołania
random(). - Funkcja opisuje interfejs bez żadnych efektów ubocznych, takich jak modyfikowanie właściwości lub zmiennych globalnych.
Ogólnie rzecz biorąc, wszystkie funkcje kompozycyjne muszą być napisane z tymi właściwościami z powodów omówionych w sekcji Ponowne komponowanie.
- Funkcja działa tak samo, gdy jest wywoływana wielokrotnie z tym samym argumentem, i nie używa innych wartości, takich jak zmienne globalne czy wywołania
Zmiana paradygmatu na deklaratywny
W przypadku wielu imperatywnych zestawów narzędzi do tworzenia interfejsów obiektowych interfejs użytkownika jest inicjowany przez utworzenie instancji drzewa widżetów. Często odbywa się to przez rozszerzenie pliku układu XML. Każdy widżet utrzymuje własny stan wewnętrzny i udostępnia metody pobierania i ustawiania, które umożliwiają logice aplikacji interakcję z widżetem.
W deklaratywnym podejściu Compose widżety są stosunkowo bezstanowe i nie udostępniają funkcji ustawiających ani pobierających. W rzeczywistości widżety nie są udostępniane jako obiekty.
Interfejs aktualizujesz, wywołując tę samą funkcję kompozycyjną z różnymi argumentami. Upraszcza to przekazywanie stanu do wzorców architektury, takich jak ViewModel, co opisano w przewodniku po architekturze aplikacji. Następnie funkcje kompozycyjne odpowiadają za przekształcanie bieżącego stanu aplikacji w interfejs za każdym razem, gdy aktualizują się dane podlegające obserwacji.
Gdy użytkownik wchodzi w interakcję z interfejsem, wywołuje on zdarzenia takie jak onClick.
Te zdarzenia powinny powiadamiać logikę aplikacji, która może następnie zmieniać jej stan.
Gdy stan się zmieni, funkcje kompozycyjne są wywoływane ponownie z nowymi danymi. Powoduje to ponowne narysowanie elementów interfejsu. Ten proces nazywa się rekompozycją.
Zawartość dynamiczna
Funkcje kompozycyjne są pisane w języku Kotlin, a nie w XML, więc mogą być tak dynamiczne jak każdy inny kod Kotlin. Załóżmy na przykład, że chcesz utworzyć interfejs, który wita listę użytkowników:
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
Ta funkcja przyjmuje listę imion i generuje powitanie dla każdego użytkownika.
Funkcje typu „composable” mogą być dość zaawansowane. Za pomocą instrukcji if możesz określić, czy chcesz wyświetlać dany element interfejsu. Możesz używać pętli. Możesz wywoływać funkcje pomocnicze. Masz pełną elastyczność języka bazowego.
Ta moc i elastyczność to jedna z głównych zalet Jetpack Compose.
Rekompozycja
W imperatywnym modelu interfejsu użytkownika, aby zmienić widżet, wywołujesz na nim funkcję ustawiającą, która zmienia jego stan wewnętrzny. W Compose ponownie wywołujesz funkcję kompozycyjną z nowymi danymi. Spowoduje to ponowne skomponowanie funkcji – widżety wygenerowane przez funkcję zostaną w razie potrzeby ponownie narysowane z nowymi danymi. Platforma Compose może inteligentnie ponownie komponować tylko te komponenty, które uległy zmianie.
Weźmy na przykład tę funkcję kompozycyjną, która wyświetla przycisk:
@Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } }
Za każdym razem, gdy użytkownik kliknie przycisk, wywołujący aktualizuje wartość clicks.
Funkcja Compose ponownie wywołuje funkcję lambda z funkcją Text, aby wyświetlić nową wartość. Ten proces nazywa się rekompozycją. Inne funkcje, które nie zależą od wartości, nie są ponownie komponowane.
Jak już wspomnieliśmy, ponowne tworzenie całego drzewa interfejsu może być kosztowne pod względem obliczeniowym, co zużywa moc obliczeniową i baterię. Compose rozwiązuje ten problem dzięki inteligentnej rekompozycji.
Ponowne komponowanie to proces ponownego wywoływania funkcji kompozycyjnych, gdy zmieniają się dane wejściowe. Gdy funkcja Compose ponownie komponuje na podstawie nowych danych wejściowych, wywołuje tylko funkcje lub wyrażenia lambda, które mogły ulec zmianie, a pozostałe pomija. Pomijając funkcje lub wyrażenia lambda z niezmienionymi parametrami, Compose ponownie komponuje interfejs w efektywny sposób.
Nigdy nie polegaj na efektach ubocznych wykonywania funkcji kompozycyjnych, ponieważ ponowna kompozycja funkcji może zostać pominięta. Jeśli to zrobisz, użytkownicy mogą zauważyć w aplikacji dziwne i nieprzewidywalne zachowania. Efekt uboczny to każda zmiana widoczna dla pozostałej części aplikacji. Na przykład te działania są niebezpiecznymi efektami ubocznymi:
- Zapisywanie w usłudze powiązanej z udostępnionym obiektem
- Aktualizowanie obiektu obserwowanego w usłudze
ViewModel - Aktualizowanie udostępnionych ustawień
Funkcje kompozycyjne mogą być wykonywane ponownie nawet w każdej klatce, np. podczas renderowania animacji. Funkcje kompozycyjne powinny działać szybko, aby uniknąć zacinania się animacji. Jeśli musisz wykonać kosztowne operacje, np. odczyt z ustawień udostępnionych, zrób to w korutynie w tle i przekaż wynik wartości do funkcji kompozycyjnej jako parametr.
Na przykład ten kod tworzy funkcję kompozycyjną, która aktualizuje wartość w SharedPreferences. Funkcja kompozycyjna nie powinna sama odczytywać ani zapisywać danych w udostępnionych preferencjach. Zamiast tego przenosi on odczyt i zapis do ViewModel w korutynie w tle. Logika aplikacji przekazuje bieżącą wartość z wywołaniem zwrotnym, aby wywołać aktualizację.
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
W tym dokumencie omawiamy kilka kwestii, o których należy pamiętać podczas korzystania z funkcji Pisanie:
- Ponowna kompozycja pomija jak najwięcej funkcji typu „composable” i wyrażeń lambda.
- Rekompozycja jest optymistyczna i może zostać anulowana.
- Funkcja typu „composable” może być uruchamiana dość często, nawet w każdej klatce animacji.
- Funkcje typu „composable” mogą być wykonywane równolegle.
- Funkcje kompozycyjne mogą być wykonywane w dowolnej kolejności.
W kolejnych sekcjach dowiesz się, jak tworzyć funkcje kompozycyjne, które obsługują ponowną kompozycję. W każdym przypadku najlepszym rozwiązaniem jest utrzymywanie szybkości, idempotentności i braku efektów ubocznych funkcji kompozycyjnych.
Ponowne komponowanie pomija jak najwięcej
Gdy części interfejsu są nieprawidłowe, Compose stara się ponownie skomponować tylko te części, które wymagają aktualizacji. Oznacza to, że może pominąć ponowne uruchomienie pojedynczego komponentu Button bez wykonywania żadnych komponentów wyżej lub niżej w drzewie interfejsu.
Każda funkcja typu „composable” i lambda może zostać ponownie skomponowana samodzielnie. Poniższy przykład pokazuje, jak rekompozycja może pominąć niektóre elementy podczas renderowania listy:
/** * Display a list of names the user can click with a header */ @Composable fun NamePicker( header: String, names: List<String>, onNameClicked: (String) -> Unit ) { Column { // this will recompose when [header] changes, but not when [names] changes Text(header, style = MaterialTheme.typography.bodyLarge) HorizontalDivider() // LazyColumn is the Compose version of a RecyclerView. // The lambda passed to items() is similar to a RecyclerView.ViewHolder. LazyColumn { items(names) { name -> // When an item's [name] updates, the adapter for that item // will recompose. This will not recompose when [header] changes NamePickerItem(name, onNameClicked) } } } } /** * Display a single name the user can click. */ @Composable private fun NamePickerItem(name: String, onClicked: (String) -> Unit) { Text(name, Modifier.clickable(onClick = { onClicked(name) })) }
Każdy z tych zakresów może być jedyną rzeczą, która zostanie wykonana podczas rekompozycji.
Funkcja Compose może przejść do lambdy Column bez wykonywania żadnych jej elementów nadrzędnych, gdy zmieni się wartość header. Podczas wykonywania Column funkcja Compose może pominąć elementy LazyColumn, jeśli names nie uległo zmianie.
Pamiętaj, że wszystkie funkcje kompozycyjne i lambdy powinny być wolne od efektów ubocznych. Jeśli musisz wykonać efekt uboczny, wywołaj go z funkcji zwrotnej.
Rekompozycja jest optymistyczna
Ponowne komponowanie rozpoczyna się, gdy Compose uzna, że parametry funkcji kompozycyjnej mogły ulec zmianie. Rekompozycja jest optymistyczna, co oznacza, że Compose oczekuje zakończenia rekompozycji, zanim parametry ponownie się zmienią. Jeśli parametr zmieni się przed zakończeniem rekompozycji, Compose może anulować rekompozycję i uruchomić ją ponownie z nowym parametrem.
Gdy rekompozycja zostanie anulowana, Compose odrzuci drzewo interfejsu z rekompozycji. Jeśli masz efekty uboczne, które zależą od wyświetlania interfejsu, zostaną one zastosowane nawet wtedy, gdy kompozycja zostanie anulowana. Może to prowadzić do niespójnego stanu aplikacji.
Sprawdź, czy wszystkie funkcje kompozycyjne i wyrażenia lambda są idempotentne i nie mają efektów ubocznych, aby obsługiwać optymistyczną rekompozycję.
Funkcje typu „composable” mogą być uruchamiane dość często.
W niektórych przypadkach funkcja typu „composable” może być uruchamiana dla każdej klatki animacji interfejsu. Jeśli funkcja wykonuje kosztowne operacje, takie jak odczyt z pamięci urządzenia, może to powodować zacinanie się interfejsu.
Jeśli na przykład widżet próbowałby odczytać ustawienia urządzenia, mógłby to robić setki razy na sekundę, co miałoby katastrofalny wpływ na wydajność aplikacji.
Jeśli funkcja typu „composable” potrzebuje danych, zdefiniuj parametry tych danych. Możesz wtedy przenieść kosztowne operacje do innego wątku poza kompozycją i przekazać wynikową wartość do funkcji typu „composable” jako parametr za pomocą mutableStateOf lub LiveData.
Funkcje typu „composable” mogą być uruchamiane równolegle
Compose może optymalizować ponowne komponowanie, uruchamiając funkcje kompozycyjne równolegle. Dzięki temu Compose będzie mogło korzystać z wielu rdzeni i uruchamiać funkcje kompozycyjne, które nie są widoczne na ekranie, z niższym priorytetem.
Ta optymalizacja oznaczałaby, że funkcja typu „composable” może być wykonywana w puli wątków w tle.
Jeśli funkcja typu „composable” wywołuje funkcję w ViewModel, Compose może wywołać tę funkcję z kilku wątków jednocześnie.
Aby sprawdzić, czy aplikacja działa prawidłowo, wszystkie funkcje kompozycyjne nie powinny mieć efektów ubocznych. Zamiast tego wywołuj efekty uboczne z poziomu wywołań zwrotnych, takich jak onClick, które zawsze są wykonywane w wątku UI.
Gdy wywoływana jest funkcja typu „composable”, wywołanie może nastąpić w innym wątku niż element wywołujący. Oznacza to, że należy unikać kodu, który modyfikuje zmienne w funkcji lambda z adnotacją @Composable, ponieważ nie jest on bezpieczny dla wątków i jest niedopuszczalnym efektem ubocznym funkcji lambda z adnotacją @Composable.
Oto przykład funkcji kompozycyjnej, która wyświetla listę i jej liczbę:
@Composable fun ListComposable(myList: List<String>) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } }
Ten kod nie ma efektów ubocznych i przekształca listę wejściową w interfejs. To świetny kod do wyświetlania krótkiej listy. Jeśli jednak funkcja zapisuje dane w zmiennej lokalnej, ten kod nie będzie bezpieczny dla wątków ani prawidłowy:
@Composable fun ListWithBug(myList: List<String>) { var items = 0 Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Card { Text("Item: $item") items++ // Avoid! Side-effect of the column recomposing. } } } Text("Count: $items") } }
W tym przykładzie wartość items jest modyfikowana przy każdej ponownej kompozycji. Może to być każda klatka animacji lub moment aktualizacji listy. W obu przypadkach interfejs użytkownika wyświetli nieprawidłową liczbę. Z tego powodu takie zapisy nie są obsługiwane w Compose. Zakazując ich, umożliwiamy frameworkowi zmianę wątków w celu wykonywania funkcji lambda z możliwością komponowania.
Funkcje typu „composable” mogą być wykonywane w dowolnej kolejności
Jeśli przyjrzysz się kodowi funkcji typu „composable”, możesz założyć, że kod jest wykonywany w kolejności, w jakiej się pojawia. Nie musi to być jednak prawda. Jeśli funkcja typu „composable” zawiera wywołania innych funkcji typu „composable”, mogą one być wykonywane w dowolnej kolejności. Compose może rozpoznawać, że niektóre elementy interfejsu mają wyższy priorytet niż inne, i rysować je w pierwszej kolejności.
Załóżmy, że masz taki kod, który rysuje 3 ekrany w układzie kart:
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
Połączenia z numerami StartScreen, MiddleScreen i EndScreen mogą być nawiązywane w dowolnej kolejności. Oznacza to, że nie możesz na przykład ustawić w funkcji StartScreen() jakiejś zmiennej globalnej (efektu ubocznego), a następnie wykorzystać tej zmiany w funkcji MiddleScreen(). Każda z tych funkcji musi być samodzielna.
Więcej informacji
Więcej informacji o tym, jak myśleć w kategoriach Compose i funkcji kompozycyjnych, znajdziesz w tych materiałach.
Filmy
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Kotlin w Jetpack Compose
- Stan i Jetpack Compose
- Warstwy architektury Jetpack Compose