Tworzenie

Jetpack Compose to nowoczesny pakiet narzędzi deklaratywnych UI na Androida. Narzędzie Compose ułatwia pisanie i obsługę interfejsu użytkownika aplikacji dzięki udostępnianiu deklaratywnego interfejsu API, który umożliwia renderowanie interfejsu aplikacji bez konieczności bezterminowej mutacji widoków frontendu. Terminologia wymaga wyjaśnień, ale znaczenie ma znaczenie dla projektu aplikacji.

paradygmat programowania deklaratywnego

Dawniej hierarchię widoku Androida można było przedstawić jako drzewo widżetó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 przechodzenie po drzewie za pomocą funkcji takich jak findViewById() oraz zmienianie węzłów przez wywoływanie 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 aktualizacji jednego z widoków danych. Łatwo jest też utworzyć stany niezgodne z prawem, gdy dwie aktualizacje powodują konflikty 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. 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 UI, który znacznie upraszcza prace inżynierskie związane z tworzeniem i aktualizowaniem interfejsów. Metoda polega na koncepcyjnym ponownym wygenerowaniu całego ekranu od zera, a następnie wprowadzeniem tylko niezbędnych zmian. Takie podejście pozwala uniknąć skomplikowanego procesu ręcznego aktualizowania hierarchii widoków stanowych. Compose to deklaratywna platforma interfejsu użytkownika.

Jednym z problemów związanych z ponownym wygenerowaniem całego ekranu jest to, że jest to potencjalnie kosztowne pod względem czasu, mocy obliczeniowej i wykorzystania 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ą.

Zrzut ekranu telefonu z widocznym tekstem

Rysunek 1. Prosta funkcja kompozycyjna, która przekazuje dane i używa ich do renderowania widżetu tekstowego na ekranie.

Kilka ważnych informacji o tej funkcji:

  • Do funkcji dodano adnotację @Composable. Wszystkie funkcje kompozycyjne muszą zawierać tę adnotację. Informuje ona kompilatora Compose, że ta funkcja ma konwertować dane na interfejs użytkownika.

  • Funkcja pobiera dane. Funkcje kompozycyjne mogą akceptować parametry, które umożliwiają logikę aplikacji opisywanie interfejsu użytkownika. W tym przypadku widżet akceptuje String, więc może powitać 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 kompozycyjne tworzą hierarchię interfejsu użytkownika przez wywoływanie innych funkcji kompozycyjnych.

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

  • Jest ona szybka, idempotentna i nie ma efektów ubocznych.

    • Funkcja działa w ten sam sposób w przypadku wielokrotnego wywoływania tego samego argumentu i nie używa innych wartości, takich jak zmienne globalne czy 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 rzecz biorąc, wszystkie funkcje kompozycyjne powinny być zapisywane z tymi właściwościami, co jest omówione w sekcji Zmiana kompozycji.

Deklaratywna zmiana paradygmatu

Korzystanie z wielu niezbędnych narzędzi UI zorientowanych na obiekt wymaga inicjowania interfejsu przez inicjowanie drzewa widżetów. Zwykle polega to na nadmuchaniu pliku układu XML. Każdy widżet zachowuje własny stan wewnętrzny i udostępnia metody pobierania i ustawienia, które pozwalają logice aplikacji na interakcję z widżetem.

W metodzie deklaratywnej Compose widżety są względnie bezstanowe i nie ujawniają funkcji ustalających ani pobierających. Widżety nie są wyświetlane jako obiekty. Zaktualizujesz interfejs, wywołując tę samą funkcję kompozycyjną z różnymi argumentami. Ułatwia to wskazanie wzorcó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 tworzenia wiadomości – od obiektów wysokiego poziomu aż do ich elementów podrzędnych.

Rysunek 2. Logika aplikacji dostarcza dane do funkcji kompozycyjnej najwyższego poziomu. Funkcja ta wykorzystuje dane do opisania UI przez wywoływanie innych elementów kompozycyjnych, a następnie przekazuje odpowiednie dane do tych elementów kompozycyjnych i dalej w hierarchii.

Gdy użytkownik wejdzie w interakcję z interfejsem, wywoła on zdarzenia takie jak onClick. Te zdarzenia powinny powiadamiać logikę aplikacji, która może potem zmienić jej stan. Gdy stan się zmieni, funkcje kompozycyjne są wywoływane ponownie z nowymi danymi. Powoduje to, że elementy interfejsu są ponownie rysowane. Ten proces nazywamy ponowną kompozycjami.

Ilustracja przedstawiająca, jak elementy interfejsu reagują na interakcję, wywołując zdarzenia obsługiwane przez logikę aplikacji.

Rysunek 3. Użytkownik wszedł w interakcję z elementem interfejsu, co spowodowało wywołanie zdarzenia. Logika aplikacji odpowiada na zdarzenie, a potem w razie potrzeby funkcje kompozycyjne są automatycznie wywoływane 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 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 kompozycyjne bywają dość zaawansowane. Aby określić, czy chcesz wyświetlić konkretny element interfejsu, możesz korzystać z instrukcji if. Możesz użyć pętli. Możesz wywoływać funkcje pomocnicze. Masz do dyspozycji pełną swobodę języka źródłowego. Ta moc i elastyczność to główne zalety Jetpack Compose.

Zmiana kompozycji

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 Compose wywołujesz w niej ponownie funkcję kompozycji z nowymi danymi. Powoduje to ponowne skomponowanie – widżety utworzone przez funkcję są w razie potrzeby ponownie rysowane z nowymi danymi. Platforma tworzenia wiadomości może inteligentnie ponownie komponować tylko te komponenty, które się zmieniły.

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")
    }
}

Po każdym kliknięciu przycisku aplikacja wywołująca 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ż 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 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. Dzięki pomijaniu wszystkich funkcji i elementów lambda, które nie mają zmienionych parametrów, funkcja Compose może sprawnie tworzyć nowe kompozycje.

Nigdy nie polegają na efektach ubocznych wykonywania funkcji kompozycyjnych, ponieważ zmiana kompozycji funkcji może zostać pominięta. 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 we właściwości udostępnionego obiektu
  • Aktualizuję element do obserwacji w: ViewModel
  • Aktualizuję wspólne ustawienia

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. odczyt ze wspólnych preferencji, zrób to w współdziałaniu w 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ść z wywołaniem zwrotnym, aby aktywować aktualizację.

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

W tym dokumencie omówiono kilka kwestii, o których należy pamiętać podczas korzystania z funkcji Utwórz:

  • Funkcje kompozycyjne mogą być wykonywane w dowolnej kolejności.
  • Funkcje kompozycyjne mogą być wykonywane równolegle.
  • Zmiana kompozycji pomija jak najwięcej funkcji kompozycyjnych i lambda.
  • Zmiana kompozycji jest optymalna i może zostać anulowana.
  • Funkcja kompozycyjna może być uruchamiana dość często, tak samo jak każda klatka animacji.

W sekcjach poniżej dowiesz się, jak tworzyć funkcje kompozycyjne służące do zmiany kompozycji. W każdym przypadku najlepiej zadbać o to, aby funkcje kompozycyjne były szybkie, idempotentne i wolne od efektów ubocznych.

Funkcje kompozycyjne 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 musi to być jednak prawdą. Jeśli funkcja kompozycyjna zawiera wywołania innych funkcji kompozycyjnych, mogą one działać w dowolnej kolejności. Funkcja tworzenia umożliwia rozpoznawanie, że niektóre elementy interfejsu mają wyższy priorytet niż inne, i wybranie ich jako pierwszego.

Załóżmy na przykład, że masz kod do rysowania trzech ekranów w układzie kart:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

Wywołania StartScreen, MiddleScreen i EndScreen mogą być wykonywane 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(). Każda z tych funkcji musi być niezależna.

Funkcje kompozycyjne mogą działać równolegle

Tworzenie może zoptymalizować zmianę kompozycji, uruchamiając funkcje kompozycyjne równolegle. Dzięki temu funkcja Compose może korzystać z wielu rdzeni i uruchamiać funkcje kompozycyjne nie na ekranie z niższym priorytetem.

Ta optymalizacja oznacza, że funkcja kompozycyjna może być wykonywana w puli wątków w tle. Jeśli funkcja kompozycyjna wywołuje funkcję w elemencie ViewModel, funkcja Compose 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 aktywuj efekty uboczne z wywołań zwrotnych, takich jak onClick, które zawsze są wykonywane w wątku interfejsu.

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 funkcji kompozycyjnej, która wyświetla listę i liczbę jej 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 krótkiej listy. Jeśli jednak funkcja zapisuje w zmiennej lokalnej, ten kod nie będzie bezpieczny ani poprawny:

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                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żda klatka animacji lub aktualizacja listy. W obu przypadkach interfejs wyświetli niewłaściwą 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.

Dostosowywanie jest w miarę możliwości pomijane

Gdy fragmenty interfejsu użytkownika są nieprawidłowe, zespół tworzenia wiadomości stara się ponownie utworzyć tylko te fragmenty, które mają być zaktualizowane. Oznacza to, że może on przejść do ponownego uruchomienia funkcji kompozycyjnej pojedynczego przycisku bez wykonywania któregoś z elementów kompozycyjnych 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 pomijać 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)
        Divider()

        // 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órą należy wykonać podczas ponownej kompozycji. 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. Z kolei podczas wykonywania funkcji Column funkcja tworzenia wiadomości może pominąć elementy z elementu LazyColumn, jeśli names nie uległ zmianie.

Również w tym przypadku wykonywanie wszystkich funkcji kompozycyjnych i lambda powinno odbywać się bez efektów ubocznych. Jeśli chcesz wykonać efekt uboczny, wywołaj go za pomocą wywołania zwrotnego.

Zmiana kompozycji jest optymistyczna

Ponowne komponowanie rozpoczyna się zawsze, gdy funkcja tworzenia uzna, że parametry funkcji kompozycyjnej mogły się zmienić. Przekomponowanie jest optymistyczne, co oznacza, że funkcja tworzenia wiadomości musi dokończyć przekomponowanie, zanim znowu ulegną zmianie parametry. 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.

Po anulowaniu ponownej kompozycji funkcja tworzenia usuwa drzewo interfejsu z ponownej kompozycji. Jeśli występują efekty uboczne zależne od wyświetlanego interfejsu, efekt uboczny będzie stosowany nawet po anulowaniu kompozycji. Może to prowadzić do niespójnego stanu aplikacji.

Aby zapewnić optymistyczną zmianę kompozycji, wszystkie funkcje kompozycyjne i lambdy muszą być idempotentne i wolne od skutków ubocznych.

Funkcje kompozycyjne mogą być uruchamiane dość często

W niektórych przypadkach funkcja kompozycyjna może być uruchamiana dla każdej klatki animacji w interfejsie. 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 widżet próbuje odczytać ustawienia urządzenia, może je odczytywać setki razy na sekundę, co może negatywnie wpłynąć na wydajność aplikacji.

Jeśli funkcja kompozycyjna wymaga danych, powinna zdefiniować parametry danych. Następnie możesz przenieść drogie zadania do innego wątku poza kompozycją i przekazać dane do tworzenia wiadomości za pomocą funkcji mutableStateOf lub LiveData.

Więcej informacji

Aby dowiedzieć się więcej o tym, jak myśleć w funkcjach tworzenia i kompozycji, zapoznaj się z dodatkowymi materiałami poniżej.

Filmy