Tworzenie

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. Stan aplikacji zmienia się na przykład z powodu interakcji z użytkownikiem, dlatego hierarchia UI musi być aktualizowana, 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ą wewnętrzny stan 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. Aktualizacja może na przykład próbować ustawić wartość węzła, który został właśnie usunięty z interfejsu użytkownika. Ogólnie rzecz biorąc, złożoność konserwacji oprogramowania wzrasta wraz z liczbą widoków wymagających aktualizacji.

W ciągu ostatnich kilku lat cała branża zaczęła przechodzić na deklaratywny model UI, który znacznie upraszcza pracę nad tworzeniem i aktualizowaniem 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ć ten koszt, Compose inteligentnie wybiera, które części interfejsu należy na bieżąco rysować ponownie. Ma to pewne konsekwencje dla sposobu projektowania komponentów interfejsu użytkownika, o którym mowa w artykule Recomposition.

Prosta funkcja typu „composable”

Za pomocą Compose możesz tworzyć interfejs użytkownika, definiując zestaw funkcji kompozytowych, które pobierają dane i wydają elementy UI. Prostym przykładem jest widżet Greeting, który przyjmuje String i wydaje widżet Text, wyświetlający wiadomość powitalną.

Zrzut ekranu telefonu z tekstem „Hello World” i kodem prostej funkcji kompozycyjnej, która generuje ten interfejs

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 kompozycyjnej Text(), która faktycznie tworzy tekstowy element interfejsu użytkownika. Funkcje typu composable generują hierarchię UI, wywołując inne funkcje typu composable.

  • Funkcja nie zwraca niczego. Funkcje kompozytowe, które emitują interfejs użytkownika, nie muszą zwracać niczego, ponieważ opisują pożądany stan ekranu zamiast tworzyć widżety interfejsu.

  • Jest ona 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 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.

Zmiana paradygmatu na deklaratywny

W przypadku wielu imperatywnych pakietów narzędzi interfejsu użytkownika zorientowanych obiektowo inicjalizacja interfejsu odbywa się przez utworzenie 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. Interfejs użytkownika aktualizujesz, wywołując tę samą funkcję składającą się z innych argumentów. 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 Twoje obiekty kompozycyjne odpowiadają za przekształcenie bieżącego stanu aplikacji w interfejs przy każdej aktualizacji dostępnych danych.

Ilustracja przepływu danych w interfejsie Compose, od obiektów najwyższego poziomu do ich podrzędnych elementów.

Rysunek 2. Logika aplikacji dostarcza danych do funkcji składanej 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 kompozytowe zostaną ponownie wywołane z nowymi danymi. Powoduje to, że elementy interfejsu są ponownie rysowane. Ten proces nazywamy ponowną kompozycjami.

Ilustracja pokazująca, jak elementy interfejsu reagują na interakcje, wywołując zdarzenia, które są obsługiwane przez logikę aplikacji.

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 kompozycyjne są napisane w kotlinie, a nie w języku XML, więc mogą być tak dynamiczne jak każdy inny kod Kotlin. Załóżmy na przykład, że chcesz utworzyć interfejs użytkownika, który wyświetla listę użytkowników:

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}

Ta funkcja przyjmuje listę nazwisk i generuje pozdrowienie dla każdego użytkownika. Funkcje kompozycyjne bywają 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 UI, aby zmienić widżet, wywołujesz metodę settera, która zmienia 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. Framework Compose może inteligentnie przekomponować tylko te komponenty, które uległy zmianie.

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. Funkcja Utwórz wywołuje jeszcze raz funkcję lambda z funkcją Text, aby wyświetlić nową wartość. Ten proces nazywa się ponowną kompozycjami. Inne funkcje, które nie zależą od wartości, nie są kompilowane ponownie.

Jak już wspomnieliśmy, ponowne tworzenie całego drzewa interfejsu może być kosztowne pod względem obliczeniowym, co zużywa moc obliczeniową i czas 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 zmienią się dane wejściowe funkcji. Gdy funkcja Utwórz ponownie kompiluje dane na podstawie nowych danych wejściowych, wywołuje tylko te funkcje lub elementy lambda, które mogły się zmienić, i pomija pozostałe. 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 tak zrobisz, użytkownicy mogą zauważyć dziwne i nieprzewidywalne działanie aplikacji. Skutkiem ubocznym jest każda zmiana widoczna w pozostałych częściach aplikacji. Na przykład te działania są niebezpiecznymi skutkami ubocznymi:

  • Zapisywanie właściwości obiektu udostępnionego
  • Aktualizowanie observable w ViewModel
  • Aktualizowanie udostępnionych ustawień

Funkcje kompozycyjne mogą być wykonywane ponownie tak często jak każda klatka, np. przy renderowaniu animacji. Funkcje kompozycyjne powinny być szybkie, by uniknąć zacinania się podczas animacji. Jeśli musisz wykonać kosztowne operacje, np. odczytać dane z wspólnych preferencji, zrób to w tle za pomocą coroutine i przekaż wynik jako parametr do funkcji kompozytowej.

Na przykład ten kod tworzy funkcję kompozycyjną aktualizującą wartość w SharedPreferences. Składnik nie może odczytywać ani zapisywać danych z wspólnych preferencji. Zamiast tego kod przenosi operacje odczytu i zapisu do ViewModel 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 kompozycyjna może być uruchamiana dość często, tak samo jak każda klatka 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.

Rekompozycja pomija jak najwięcej

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 ono pominąć ponowne wykonanie komponentu w przypadku pojedynczego przycisku bez wykonywania żadnych innych komponentów znajdujących się powyżej lub poniżej niego w drzewie interfejsu.

Każda funkcja kompozycyjna i lambda mogą się ponownie skomponować. Oto przykład, który 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ć 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 za pomocą wywołania zwrotnego.

Rekompozycja jest optymistyczna

Ponowne komponowanie rozpoczyna się zawsze, gdy funkcja tworzenia uzna, że parametry funkcji kompozycyjnej 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 się zmieni przed zakończeniem rekompozycji, usługa Compose może anulować rekompozycję i ponownie ją uruchomić 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.

Upewnij się, że wszystkie funkcje i funkcje typu lambda, które można łączyć, są idempotentne i nie mają efektów ubocznych, aby można było stosować optymistyczną rekompozycję.

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, takie jak odczyt z pamięci urządzenia, może powodować zakłócenia interfejsu.

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 kompozycyjne można uruchamiać równolegle

Compose może optymalizować rekompozycję, wykonując funkcje kompozytowe równolegle. Dzięki temu funkcja Compose mogła korzystać z wielu rdzeni do uruchamiania funkcji kompozycyjnych nie na ekranie z niższym priorytetem.

Ta optymalizacja oznacza, że funkcja składana może być wykonywana w grupie 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 modyfikującego zmienne w komponencie lambda – zarówno dlatego, że nie jest on bezpieczny w wątkach, jak i ponieważ jest to niedopuszczalny efekt uboczny funkcji kompozycyjnej 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 efektów ubocznych i przekształca listę wejściową w UI. To świetny kod do wyświetlania małej listy. Jeśli jednak funkcja zapisuje w zmiennej lokalnej, ten kod nie będzie bezpieczny 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 zmianie kompozycji. 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 takie zapisy nie są obsługiwane w Compose. Zabraniając tych zapisów, pozwalamy frameworkowi na zmianę wątków w celu wykonania kompozytowanych funkcji lambda.

Funkcje typu „composable” mogą być wykonywane w dowolnej kolejności

Jeśli spojrzysz na kod funkcji składanej, możesz założyć, że kod jest wykonywany w kolejności, w jakiej się pojawia. Nie ma jednak gwarancji, że tak się stanie. Jeśli funkcja kompozycyjna zawiera wywołania innych funkcji kompozycyjnych, mogą one działać 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

Obecnie nie ma rekomendacji.

na swoje konto Google.