Jetpack Compose to nowoczesny deklaratywny zestaw narzędzi interfejsu na Androida. Compose ułatwia pisanie i utrzymywanie interfejsu aplikacji dzięki deklaratywnemu interfejsowi 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 hierarchię widoków Androida można przedstawić 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 wystąpienia błędów. Jeśli dane są renderowane w wielu miejscach, łatwo zapomnieć o zaktualizowaniu jednego z widoków, w których są wyświetlane. Łatwo też utworzyć nieprawidłowe stany, gdy dwie aktualizacje w nieoczekiwany sposób ze sobą kolidują. 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 znacznie upraszcza proces tworzenia i aktualizowania interfejsów. Ta technika polega na koncepcyjnym odtworzeniu całego ekranu od zera, a następnie zastosowaniu tylko niezbędnych zmian. Dzięki temu nie musisz ręcznie aktualizować hierarchii widoków z zachowywaniem stanu. Compose to deklaratywny framework 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 wpływ na sposób projektowania komponentów interfejsu, co opisano w sekcji Ponowne komponowanie.
Prosta 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. Prostym przykładem jest widżet Greeting
, który przyjmuje widżet String
i emituje widżet Text
wyświetlający powitanie.
Rysunek 1. Prosta funkcja kompozycyjna, do której przekazywane są dane, a ona używa ich do renderowania widżetu tekstowego na ekranie.
Kilka ważnych informacji o tej funkcji:
Funkcja jest opatrzona adnotacją
@Composable
. Wszystkie funkcje Composable muszą mieć tę adnotację. Informuje ona kompilator Compose, że ta funkcja ma przekształcać dane w interfejs.Funkcja pobiera dane. Funkcje kompozycyjne mogą przyjmować parametry, które umożliwiają logice aplikacji opisanie interfejsu. W tym przypadku widżet akceptuje
String
, dzięki czemu może powitać użytkownika po imieniu.Funkcja wyświetla tekst w interfejsie. W tym celu wywołuje funkcję kompozycyjną
Text()
, która tworzy element interfejsu tekstu. Funkcje typu „composable” emitują hierarchię interfejsu, wywołując inne funkcje typu „composable”.Funkcja nie zwraca żadnych wartości. Funkcje kompozycyjne, które emitują interfejs, nie muszą niczego zwracać, ponieważ opisują pożądany stan ekranu zamiast tworzyć widżety interfejsu.
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 efektów ubocznych, takich jak modyfikowanie właściwości lub zmiennych globalnych.
Ogólnie rzecz biorąc, wszystkie funkcje kompozycyjne powinny być pisane z uwzględnieniem tych właściwości 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 deklaratywnego
W przypadku wielu imperatywnych zestawów narzędzi do tworzenia interfejsu użytkownika zorientowanych obiektowo interfejs użytkownika inicjuje się 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 użytkownika aktualizujesz, wywołując tę samą funkcję kompozycyjną z różnymi argumentami. Ułatwia to przekazywanie stanu do wzorców architektonicznych, takich jak ViewModel
, zgodnie z opisem 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.
Rysunek 2. Logika aplikacji dostarcza dane do funkcji kompozycyjnej najwyższego poziomu. Ta funkcja używa danych do opisywania interfejsu przez wywoływanie innych funkcji kompozycyjnych i przekazywanie odpowiednich danych do tych funkcji oraz w dół hierarchii.
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ę ponownym komponowaniem.
Rysunek 3. Użytkownik wszedł w interakcję z elementem interfejsu, co spowodowało wywołanie zdarzenia. Logika aplikacji reaguje na zdarzenie, a funkcje kompozycyjne są w razie potrzeby automatycznie wywoływane ponownie z nowymi parametrami.
Zawartość dynamiczna
Funkcje kompozycyjne są pisane w Kotlinie, a nie w XML-u, więc mogą być tak dynamiczne jak każdy inny kod w tym języku. 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ść złożone. 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.
Recomposition
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ę ponownym komponowaniem. 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 inteligentnemu ponownemu komponowaniu.
Ponowne komponowanie to proces ponownego wywoływania funkcji kompozycyjnych, gdy zmieniają się dane wejściowe. Dzieje się tak, gdy zmieniają się dane wejściowe funkcji. Gdy funkcja Compose ponownie komponuje interfejs na podstawie nowych danych wejściowych, wywołuje tylko te funkcje lub wyrażenia lambda, które mogły ulec zmianie, a pozostałe pomija. Pomijając wszystkie funkcje lub wyrażenia lambda, które nie mają zmienionych parametrów, Compose może skutecznie ponownie komponować interfejs.
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 reszty aplikacji. Na przykład te działania są niebezpiecznymi efektami ubocznymi:
- Zapisywanie w usłudze powiązanej z obiektem udostępnionym
- Aktualizowanie obiektu obserwowanego w:
ViewModel
- Aktualizowanie ustawień udostępnianych
Funkcje kompozycyjne mogą być ponownie wykonywane 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 ustawieniach współdzielonych. Zamiast tego ten kod przenosi 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 pisania:
- Ponowne komponowanie pomija jak najwięcej funkcji kompozycyjnych i wyrażeń lambda.
- Ponowne komponowanie jest optymistyczne i może zostać anulowane.
- Funkcja kompozycyjna 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 sekcjach poniżej 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 fragmenty interfejsu są nieprawidłowe, Compose stara się ponownie skomponować tylko te fragmenty, które wymagają aktualizacji. Oznacza to, że może pominąć ponowne uruchomienie pojedynczego komponentu kompozycyjnego Button bez wykonywania żadnych komponentów kompozycyjnych powyżej lub poniżej niego w drzewie interfejsu.
Każda funkcja kompozycyjna i lambda może być ponownie komponowana samodzielnie. Oto przykład, który pokazuje, jak ponowne komponowanie 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ć jedynym elementem wykonanym podczas ponownego komponowania.
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 Pisanie może pominąć elementy LazyColumn
, jeśli names
nie uległo zmianie.
Ponownie: wykonanie wszystkich funkcji kompozycyjnych lub wyrażeń lambda nie powinno powodować efektów ubocznych. Gdy musisz wykonać efekt uboczny, wywołaj go z funkcji zwrotnej.
Ponowne komponowanie jest optymistyczne
Ponowne komponowanie rozpoczyna się, gdy Compose uzna, że parametry funkcji kompozycyjnej mogły ulec zmianie. Ponowne komponowanie jest optymistyczne, co oznacza, że Compose oczekuje zakończenia ponownego komponowania, zanim parametry ponownie się zmienią. Jeśli parametr zmieni się przed zakończeniem ponownego komponowania, Compose może anulować ponowne komponowanie i uruchomić je ponownie z nowym parametrem.
Gdy ponowne komponowanie zostanie anulowane, Compose odrzuci drzewo interfejsu z ponownego komponowania. 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.
Aby obsługiwać optymistyczną rekompozycję, upewnij się, że wszystkie funkcje kompozycyjne i wyrażenia lambda są idempotentne i nie mają efektów ubocznych.
Funkcje typu „composable” mogą być uruchamiane dość często.
W niektórych przypadkach funkcja kompozycyjna może być uruchamiana w każdej klatce animacji interfejsu. Jeśli funkcja wykonuje kosztowne operacje, takie jak odczyt z pamięci urządzenia, może 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 kompozycyjna potrzebuje danych, powinna zdefiniować parametry tych danych. Następnie możesz przenieść kosztowne operacje do innego wątku poza kompozycją i przekazać dane do Compose za pomocą mutableStateOf
lub LiveData
.
Funkcje typu „composable” można uruchamiać 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 kompozycyjna może być wykonywana w puli wątków w tle.
Jeśli funkcja kompozycyjna wywołuje funkcję w ViewModel
, Compose może wywołać tę funkcję z kilku wątków jednocześnie.
Aby zapewnić prawidłowe działanie aplikacji, wszystkie funkcje kompozycyjne nie powinny mieć efektów ubocznych. Zamiast tego wywołuj efekty uboczne z poziomu wywołań zwrotnych, np. onClick
, które zawsze są wykonywane w wątku interfejsu.
Gdy wywoływana jest funkcja kompozycyjna, wywołanie może nastąpić w innym wątku niż 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 items
jest modyfikowany przy każdej ponownej kompozycji. Może to być każda klatka animacji lub moment aktualizacji listy. W obu przypadkach interfejs użytkownika będzie wyświetlać nieprawidłową liczbę. Z tego powodu zapisy tego typu nie są obsługiwane w Compose. Zakazując takich zapisów, 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 kompozycyjnej, możesz założyć, że kod jest wykonywany w kolejności, w jakiej się pojawia. Nie ma jednak gwarancji, że tak będzie. Jeśli funkcja kompozycyjna zawiera wywołania innych funkcji kompozycyjnych, mogą one być wykonywane w dowolnej kolejności. Compose ma możliwość rozpoznawania, że niektóre elementy interfejsu użytkownika mają wyższy priorytet niż inne, i wyświetlania ich w pierwszej kolejności.
Załóżmy na przykład, ż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ć StartScreen()
zmiennej globalnej (efektu ubocznego) i sprawić, aby MiddleScreen()
wykorzystało tę zmianę. Każda z tych funkcji musi być samodzielna.
Więcej informacji
Aby dowiedzieć się więcej o sposobie myślenia w Compose i funkcjach kompozycyjnych, zapoznaj się z tymi dodatkowymi materiałami.
Filmy
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Kotlin w Jetpack Compose
- Stan i Jetpack Compose
- Warstwy architektury Jetpack Compose