Jetpack Compose to nowoczesny deklaratywny zestaw narzędzi interfejsu dla Androida. Compose ułatwia pisanie i utrzymywanie interfejsu aplikacji, ponieważ udostępnia deklaratywny interfejs API, który umożliwia renderowanie interfejsu aplikacji bez imperatywnego modyfikowania widoków front-endu. Terminologia wymaga wyjaśnienia, ale jej konsekwencje są ważne dla projektu aplikacji.
Paradygmat programowania deklaratywnego
Hierarchia widoków w Androidzie była dotychczas reprezentowana jako drzewo widgetów interfejsu. Gdy stan aplikacji zmienia się z powodu takich czynników jak interakcje użytkowników, hierarchia UI musi być zaktualizowana, aby wyświetlać aktualne dane.
Najczęstszym sposobem aktualizowania interfejsu jest przejście po drzewie za pomocą funkcji takich jak findViewById()
oraz zmiana węzłów przez wywołanie 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 kilku miejscach, łatwo zapomnieć o zaktualizowaniu widoku, w którym się one wyświetlają. Łatwo też tworzyć nieprawidłowe stany, gdy 2 aktualizacje kolidują ze sobą w nieoczekiwany sposób. Na przykład aktualizacja może próbować ustawić wartość węzła, który został właśnie usunięty z interfejsu użytkownika. Złożoność konserwacji oprogramowania rośnie wraz z liczbą wyświetleń, które wymagają aktualizacji.
W ciągu ostatnich kilku lat cała branża zaczęła przechodzić na deklaratywny model interfejsu użytkownika, który znacznie upraszcza proces tworzenia i aktualizowania interfejsów. Polega ona na odtwarzaniu całego ekranu od podstaw, a następnie wprowadzaniu tylko niezbędnych zmian. Dzięki temu unikniesz złożoności ręcznego aktualizowania hierarchii widoku stanu. Compose to deklaratywny framework interfejsu użytkownika.
Jednym z wyzwań związanych z regenerowaniem całego ekranu jest to, że może to być kosztowne pod względem czasu, mocy obliczeniowej i zużycia baterii. Aby ograniczyć te koszty, funkcja Utwórz inteligentnie wybiera elementy interfejsu, które należy w każdej chwili zmienić. Ma to wpływ na sposób projektowania komponentów UI, co omówiliśmy w sekcji Zmiana kompozycji.
Prosta funkcja kompozycyjna
Za pomocą funkcji Compose możesz utworzyć interfejs użytkownika, definiując zestaw funkcji kompozycyjnych, które pobierają dane i emitują elementy interfejsu. Prostym przykładem jest widżet Greeting
, który pobiera String
i wyświetla widżet Text
wyświetlający wiadomość powitalną.
Rysunek 1. Prosta funkcja kompozytowa, która otrzymuje dane i wykorzystuje je do renderowania widżetu tekstowego na ekranie.
Kilka informacji o tej funkcji:
Funkcja jest oznaczona adnotacją
@Composable
. Wszystkie funkcje kompozytowe muszą mieć tę adnotację. Informuje ona kompilator Compose, że dana funkcja ma przekształcać dane w interfejs użytkownika.Funkcja przyjmuje dane. Funkcje kompozytowe mogą przyjmować parametry, które umożliwiają logice aplikacji opisywanie interfejsu użytkownika. W tym przypadku nasz widżet przyjmuje parametr
String
, aby witać użytkownika po imieniu.Funkcja wyświetla tekst w interfejsie. Robi to przez wywołanie funkcji składanej
Text()
, która faktycznie tworzy element interfejsu tekstowego. Funkcje typu composable generują hierarchię UI, wywołując inne funkcje typu composable.Funkcja nie zwraca niczego. Funkcje tworzenia, które generują interfejs, nie muszą niczego zwracać, ponieważ opisują pożądany stan ekranu, zamiast tworzyć widżety interfejsu.
Ta funkcja jest szybka, idempotentna i nie powoduje 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 ani wywołania funkcji
random()
. - Funkcja ta opisuje interfejs użytkownika bez żadnych efektów ubocznych, takich jak modyfikowanie właściwości czy zmiennych globalnych.
Ogólnie wszystkie funkcje z możliwością składania powinny być napisane z użyciem tych właściwości ze względu na powody opisane w sekcji Rekompozycja.
- 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 ani wywołania funkcji
Deklaratywna zmiana paradygmatu
Wiele niezbędnych narzędzi UI zorientowanych na obiekt można inicjować przez inicjowanie drzewa widżetów. Często odbywa się to przez napełnienie pliku XML układu. Każdy widget ma własny stan wewnętrzny i metody getter i setter, które umożliwiają logice aplikacji interakcję z widżetem.
W deklaratywnym podejściu Compose widżety są w większości bezstanowe i nie udostępniają funkcji setter ani getter. W rzeczywistości widżety nie są widoczne jako obiekty.
Zaktualizujesz interfejs, wywołując tę samą funkcję kompozycyjną z różnymi argumentami. Dzięki temu możesz łatwo określać stany w ramach wzorów architektonicznych, takich jak ViewModel
, jak opisano w przewodniku po architekturze aplikacji. Następnie komponenty są odpowiedzialne za przekształcanie bieżącego stanu aplikacji w interfejs za każdym razem, gdy dane obserwowalne są aktualizowane.
Rysunek 2. Logika aplikacji dostarcza dane do funkcji kompozycyjnej najwyższego poziomu. Ta funkcja używa danych do opisu interfejsu, wywołując inne komponenty, a następnie przekazuje odpowiednie dane do tych komponentów i dalej w hierarchii.
Gdy użytkownik wchodzi w interakcję z interfejsem, ten wywołuje zdarzenia takie jak onClick
.
Te zdarzenia powinny powiadomić logikę aplikacji, która może zmienić stan aplikacji.
Gdy stan się zmieni, funkcje kompozycyjne są wywoływane ponownie z nowymi danymi. Powoduje to ponowne rysowanie elementów interfejsu. Ten proces nazywa się rekompozycją.
Rysunek 3. Użytkownik wszedł w interakcję z elementem interfejsu, co spowodowało wywołanie zdarzenia. Logika aplikacji reaguje na zdarzenie, a funkcje składane są automatycznie wywoływane ponownie (w razie potrzeby z nowymi parametrami).
Zawartość dynamiczna
Funkcje składane są pisane w Kotlinie, a nie w XML, więc mogą być tak dynamiczne jak dowolny inny kod Kotlin. Załóżmy np., że chcesz utworzyć interfejs witający listę użytkowników:
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
Ta funkcja pobiera listę imion i generuje powitanie dla każdego użytkownika.
Funkcje typu „composable” mogą być dość zaawansowane. Za pomocą instrukcji if
możesz zdecydować, czy chcesz wyświetlić dany element interfejsu użytkownika. Możesz używać pętli. Możesz wywoływać funkcje pomocnicze. Masz pełną elastyczność w przypadku języka źródłowego.
Ta moc i elastyczność to główne zalety Jetpack Compose.
Rekompozycja
W imperatywnym modelu interfejsu użytkownika, aby zmienić widżet, należy wywołać w widżecie funkcję ustawiającą, która zmieni jego stan wewnętrzny. W komponencie Compose ponownie wywołujesz funkcję kompozytową, podając nowe dane. W ten sposób funkcja zostanie ponownie skompilowana – w razie potrzeby widżety emitowane przez funkcję zostaną ponownie narysowane z nowymi danymi. Platforma tworzenia wiadomości może inteligentnie ponownie komponować tylko te komponenty, które się zmieniły.
Weź pod uwagę tę funkcję składającą, 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 kliknięciem przycisku wywołujący aktualizuje wartość clicks
.
Aby wyświetlić nową wartość, funkcja Compose ponownie wywołuje funkcję lambda z funkcja Text
. Ten proces nazywa się rekompozycją. Inne funkcje, które nie zależą od wartości, nie są przekształcane.
Jak już wspominaliśmy, ponowne skompilowanie całego drzewa interfejsu może wymagać dużych nakładów obliczeniowych, ponieważ wymaga to mocy obliczeniowej i czasu pracy baterii. Usługa Compose rozwiązuje ten problem dzięki inteligentnej zmianie kompozycji.
Przekomponowanie to proces ponownego wywoływania funkcji kompozycyjnych po zmianie danych wejściowych. Dzieje się tak, gdy zmieniają się dane wejściowe funkcji. Gdy funkcja Compose ponownie tworzy wyrażenie na podstawie nowych danych wejściowych, wywołuje tylko te funkcje i funkcje lambda, które mogły ulec zmianie, a pozostałe pomija. Pomijanie wszystkich funkcji i lambda, które nie mają zmienionych parametrów, pozwala Compose efektywnie przeredagować kod.
Nigdy nie polegaj na efektach ubocznych wykonywania funkcji składanych, ponieważ może to spowodować pominięcie ich ponownego składania. Jeśli to zrobisz, użytkownicy mogą zaobserwować dziwne i nieprzewidywalne działanie w Twojej aplikacji. Skutkiem ubocznym jest każda zmiana widoczna dla reszty aplikacji. Są to na przykład niebezpieczne skutki uboczne:
- Zapisywanie właściwości obiektu udostępnionego
- Aktualizuję element do obserwacji w:
ViewModel
- Aktualizuję wspólne ustawienia
Funkcje składane mogą być ponownie wykonywane tak często, jak każda klatka, np. podczas renderowania animacji. Funkcje kompozycyjne powinny być szybkie, by uniknąć zacinania się podczas animacji. Jeśli musisz wykonać kosztowne operacje, np. odczyt ze wspólnych preferencji, zrób to w współrzędnym tle i przekazuj wynik wartości do funkcji kompozycyjnej jako parametr.
Na przykład ten kod tworzy funkcję kompozycyjną aktualizującą wartość w SharedPreferences
. Funkcja kompozycyjna nie powinna odczytywać ani zapisywać danych z udostępnionych ustawień. Zamiast tego przenosi on odczyt i zapis do ViewModel
w współdziałaniu w tle. Logika aplikacji przekazuje bieżącą wartość za pomocą funkcji wywołania zwrotnego, 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 Compose:
- Zmiana kompozycji pomija jak najwięcej funkcji kompozycyjnych i lambda.
- Rekompozycja jest optymistycznych i może zostać anulowana.
- Funkcja składana może być wykonywana dość często, nawet w każdej klatce animacji.
- Funkcje typu „composable” mogą być wykonywane równolegle.
- Funkcje składane mogą być wykonywane w dowolnej kolejności.
W kolejnych sekcjach dowiesz się, jak tworzyć funkcje złożone, które umożliwiają rekompozycję. W każdym przypadku sprawdzoną metodą jest utrzymywanie funkcji składanych w taki sposób, aby były szybkie, idempotentne i bez efektów ubocznych.
Dostosowywanie jest w miarę możliwości pomijane
Jeśli część interfejsu użytkownika jest nieprawidłowa, Compose stara się ponownie skompilować tylko te części, które wymagają aktualizacji. Oznacza to, że może on przejść do ponownego uruchomienia funkcji kompozycyjnej pojedynczego przycisku bez wykonywania któregokolwiek z elementów kompozycyjnych znajdujących się powyżej lub poniżej niego w drzewie interfejsu.
Każda funkcja składana i funkcja lambda może się sama przekształcić. Oto przykład, który pokazuje, jak zmiana kompozycji 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 zakresem wykonywanym podczas rekompozycji.
Gdy element header
się zmieni, funkcja tworzenia wiadomości może przejść do funkcji lambda Column
bez wykonywania któregoś z elementów nadrzędnych. Podczas wykonywania Column
usługa Compose może pominąć elementy LazyColumn
, jeśli names
się nie zmienił.
Ponownie, wykonywanie wszystkich funkcji składanych lub lambd nie powinno powodować efektów ubocznych. Jeśli chcesz wykonać efekt uboczny, wywołaj go z powrotem.
Rekompozycja jest optymistyczna
Rekompozycja rozpoczyna się, gdy Compose uzna, że parametry komponentu mogły się zmienić. Rekompozycja jest optymistyczna, co oznacza, że usługa Compose spodziewa się, że rekompozycja zostanie ukończona, zanim parametry zmienią się ponownie. Jeśli parametr zmieni się przed zakończeniem ponownej kompozycji, funkcja tworzenia wiadomości może anulować tę zmianę i uruchomić ją ponownie z nowym parametrem.
Gdy anulujesz rekompozycję, Compose odrzuci drzewo interfejsu użytkownika z rekompozycji. Jeśli masz jakieś efekty uboczne, które zależą od wyświetlanego interfejsu, efekt uboczny zostanie zastosowany, nawet jeśli kompozycja zostanie anulowana. Może to spowodować niespójny stan aplikacji.
Aby zapewnić optymistyczną zmianę kompozycji, wszystkie funkcje kompozycyjne i lambdy muszą być idempotentne i wolne od skutków ubocznych.
Funkcje składane mogą być wykonywane dość często.
W niektórych przypadkach funkcja składana może być wykonywana w przypadku każdego klatka animacji interfejsu użytkownika. Jeśli funkcja wykonuje kosztowne operacje, np. odczytuje dane z pamięci urządzenia, może powodować zacinanie się w interfejsie.
Jeśli na przykład widget próbuje odczytać ustawienia urządzenia, może odczytać te ustawienia setki razy na sekundę, co może mieć katastrofalne skutki dla wydajności aplikacji.
Jeśli funkcja kompozycyjna wymaga danych, powinna zdefiniować parametry danych. Następnie możesz przenieść wymagające dużych zasobów obliczeniowych operacje do innego wątku, poza kompozycją, i przekazać dane do Compose za pomocą mutableStateOf
lub LiveData
.
Funkcje składane można wykonywać równolegle
Compose może optymalizować rekompozycję, wykonując funkcje kompozytowe równolegle. Dzięki temu Compose mógłby korzystać z wielu rdzeni i uruchamiać funkcje kompozytowe poza ekranem z niższym priorytetem.
W ramach tej optymalizacji funkcja kompozycyjna może być wykonywana w puli wątków w tle.
Jeśli funkcja składana wywołuje funkcję w komponencie ViewModel
, może wywołać tę funkcję z kilku wątków jednocześnie.
Aby aplikacja działała prawidłowo, wszystkie funkcje kompozycyjne nie powinny mieć żadnych efektów ubocznych. Zamiast tego wywołuj skutki uboczne z powrotem do wywołania funkcji, takie jak onClick
, które zawsze są wykonywane w wątku interfejsu użytkownika.
Po wywołaniu funkcji kompozycyjnej wywołanie może wystąpić w innym wątku niż obiekt wywołujący. Oznacza to, że należy unikać kodu, który modyfikuje zmienne w kompozycyjnej funkcji lambda. Dzieje się tak zarówno dlatego, że taki kod nie jest bezpieczny w zakresie wątków, jak i dlatego, że jest niedopuszczalnym efektem ubocznym kompozycyjnej funkcji lambda.
Oto przykład komponentu wyświetlającego listę i liczbę elementów:
@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 powoduje żadnych efektów ubocznych i przekształca listę danych wejściowych w interfejs. To świetny kod do wyświetlania małej listy. Jeśli jednak funkcja zapisuje dane do zmiennej lokalnej, kod nie będzie bezpieczny w wielu wątkach ani poprawny:
@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 element items
jest modyfikowany przy każdej rekompozycji. Może to być każdy kadr animacji lub moment aktualizacji listy. W obu przypadkach interfejs użytkownika wyświetli nieprawidłową liczbę. Z tego powodu funkcja Compose nie obsługuje zapisów takich jak ten. Blokując te zapisy, umożliwiamy platformie zmienianie wątków w celu wykonywania funkcji lambda z możliwością kompozycyjnej.
Funkcje typu „composable” mogą być wykonywane w dowolnej kolejności
Gdy spojrzysz na kod funkcji kompozycyjnej, możesz założyć, że kod jest uruchamiany w kolejności, w jakiej jest wyświetlana. Nie ma jednak gwarancji, że tak się stanie. Jeśli funkcja składana zawiera wywołania innych funkcji składanych, mogą one być wykonywane w dowolnej kolejności. Compose może rozpoznać, że niektóre elementy interfejsu mają wyższy priorytet niż inne, i najpierw je narysować.
Załóżmy na przykład, że masz kod, który rysuje 3 ekrany w układzie z kartami:
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
Połączenia do numerów StartScreen
, MiddleScreen
i EndScreen
mogą następować w dowolnej kolejności. Oznacza to, że nie można na przykład ustawić zmiennej globalnej (efekt uboczny) za pomocą funkcji StartScreen()
i skorzystać z tej zmiany w usłudze MiddleScreen()
. Zamiast tego każda z tych funkcji musi być samodzielna.
Więcej informacji
Aby dowiedzieć się więcej o komponowaniu i funkcjach kompozytowanych, zapoznaj się z tymi dodatkowymi materiałami.
Filmy
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Kotlin w Jetpack Compose
- Stan i Jetpack Compose
- Warstwowy układ Jetpack Compose