Tworzenie

Jetpack Compose to nowoczesny, deklaratywny pakiet narzędzi UI na Androida. Tworzenie ułatwia pisanie i obsługę UI aplikacji, ponieważ udostępnia deklaratywny interfejs API, który umożliwia renderowanie interfejsu aplikacji bez konieczności mutacji widoków frontendu. Ta terminologia wymaga wyjaśnienia, ale jej konsekwencje są ważne dla projektu aplikacji.

Wzorzec programowania deklaratywnego

W przeszłości hierarchia widoków Androida była reprezentowana jako drzewo widżetów UI. Kiedy stan aplikacji zmienia się m.in. z powodu interakcji użytkowników, należy zaktualizować hierarchię interfejsu, aby wyświetlać bieżące dane. Najczęstszym sposobem aktualizowania interfejsu użytkownika jest spacer po drzewie za pomocą funkcji takich jak findViewById() i zmienianie węzłów za pomocą 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 błędów. Jeśli dane są renderowane w wielu miejscach, łatwo jest zapomnieć zaktualizować jeden z widoków, który je pokazuje. Łatwo jest też utworzyć nielegalne stany, w których 2 aktualizacje powodują konflikty w niespodziewany sposób. W ramach aktualizacji można 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ść obsługi 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 pracę nad tworzeniem i aktualizowaniem interfejsów użytkownika. Polega ona na koncepcji ponownego wygenerowania całego ekranu od zera, a potem zastosowania tylko niezbędnych zmian. To podejście pozwala uniknąć złożoności ręcznej aktualizacji hierarchii widoków stanowych. Tworzenie wiadomości to deklaratywne środowisko interfejsu użytkownika.

Jednym z problemów z odtwarzaniem całego ekranu jest to, że jest ono potencjalnie kosztowne pod względem czasu, mocy obliczeniowej i wykorzystania baterii. Aby ograniczyć ten koszt, funkcja Utwórz inteligentnie wybiera części interfejsu, które mają zostać usunięte w danym momencie. Ma to wpływ na projekt komponentów interfejsu, co zostało omówione w sekcji Zmiana kompozycji.

Prosta funkcja kompozycyjna

Korzystając z funkcji Compose, możesz utworzyć interfejs użytkownika, definiując zestaw funkcji kompozycyjnych, które pobierają dane i generują elementy interfejsu. Prosty przykład to widżet Greeting, który pobiera widżet String i emituje widżet Text, który wyświetla wiadomość powitalną.

Zrzut ekranu telefonu z widocznym tekstem

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

Kilka ważnych informacji o tej funkcji:

  • Funkcja jest oznaczona adnotacją @Composable. Wszystkie funkcje kompozycyjne muszą mieć tę adnotację. Informuje ona kompilator Compose, że ta funkcja służy do konwertowania danych na interfejs użytkownika.

  • Funkcja pobiera dane. Funkcje kompozycyjne mogą przyjmować parametry, dzięki którym logika aplikacji może opisywać interfejs użytkownika. W tym przypadku nasz widżet akceptuje String, więc może powitać użytkownika po imieniu.

  • Funkcja wyświetla tekst w interfejsie. Aby to zrobić, wywołuje funkcję kompozycyjną Text(), która faktycznie tworzy tekstowy element interfejsu. Funkcje kompozycyjne emitują hierarchię interfejsu, wywołując inne funkcje kompozycyjne.

  • Ta funkcja nie zwraca niczego. Funkcje tworzenia, które emitują interfejs użytkownika, nie muszą niczego zwracać, ponieważ opisują pożądany stan ekranu, a nie konstruować widżety UI.

  • Ta funkcja jest szybka, idempotentna i nie ma efektów ubocznych.

    • Funkcja działa tak samo po kilkukrotnym wywołaniu z tym samym argumentem i nie używa innych wartości, takich jak zmienne globalne czy wywołania random().
    • Ta funkcja opisuje interfejs bez żadnych skutków ubocznych, takich jak zmiana właściwości czy zmiennych globalnych.

    Ogólnie wszystkie funkcje kompozycyjne powinny być zapisywane z tymi właściwościami. Powody zostały omówione w sekcji Zmiana kompozycji.

Deklaratywna zmiana paradygmatu

Za pomocą wielu imperatywnych zestawów narzędzi interfejsu zorientowanych na obiekty możesz inicjować interfejs, inicjując drzewo widżetów. Często polegają na dodaniu zawyżonego pliku układu XML. Każdy widżet zachowuje własny stan wewnętrzny i udostępnia metody getter i setter, które pozwalają logice aplikacji na interakcję z widżetem.

W metodzie deklaratywnej tworzenia widżetów widżety są względnie bezstanowe i nie udostępniają funkcji ustawiających ani pobierających. W rzeczywistości widżety nie są widoczne jako obiekty. Aby zaktualizować interfejs, wywołaj tę samą funkcję kompozycyjną z różnymi argumentami. Ułatwia to określenie wzorców architektonicznych, takich jak ViewModel, zgodnie z opisem w przewodniku po architekturze aplikacji. Następnie obiekty kompozycyjne są odpowiedzialne za przekształcenie bieżącego stanu aplikacji w interfejs użytkownika przy każdej aktualizacji danych dostępnych do obserwacji.

Ilustracja przepływu danych w interfejsie tworzenia wiadomości – od obiektów ogólnych po ich elementy podrzędne.

Rysunek 2. Logika aplikacji dostarcza dane do funkcji kompozycyjnej najwyższego poziomu. Ta funkcja wykorzystuje dane do opisania interfejsu użytkownika przez wywołanie innych elementów kompozycyjnych, a następnie przekazuje odpowiednie dane do tych funkcji i w dalszym ciągu w hierarchii.

Gdy użytkownik wchodzi w interakcję z interfejsem, wywołuje on zdarzenia takie jak onClick. Zdarzenia te powinny powiadamiać działanie aplikacji, co może spowodować zmianę jej stanu. Po zmianie stanu funkcje kompozycyjne są wywoływane ponownie z nowymi danymi. W rezultacie elementy interfejsu są ponownie rysowane. Jest to tzw. rekompozycja.

Ilustracja tego, jak elementy interfejsu reagują na interakcje przez wyzwalanie zdarzeń obsługiwanych przez logikę aplikacji.

Rysunek 3. Użytkownik wszedł w interakcję z elementem interfejsu, co spowodowało uruchomienie zdarzenia. Logika aplikacji reaguje na zdarzenie, a potem w razie potrzeby funkcje kompozycyjne są automatycznie wywoływane ponownie z nowymi parametrami.

Zawartość dynamiczna

Ponieważ funkcje kompozycyjne są zapisywane w Kotlin, a nie w XML, mogą być równie dynamiczne jak każdy inny kod Kotlin. Załóżmy np., że chcesz utworzyć interfejs z listą użytkowników:

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

Ta funkcja pobiera listę imion i nazwisk i generuje powitanie dla każdego użytkownika. Funkcje składane mogą być dość zaawansowane. Za pomocą instrukcji if możesz określić, czy chcesz pokazać konkretny element interfejsu. Możesz użyć pętli. Można wywołać funkcje pomocnicze. Masz pełną swobodę wyboru języka. Ta moc i elastyczność to jedna z najważniejszych zalet Jetpack Compose.

Zmiana kompozycji

Aby zmienić widżet w imperatywnym modelu interfejsu, wywołaj w widżecie metodę ustawiającą, która zmieni jego stan wewnętrzny. W oknie Compose ponownie wywołujesz funkcję kompozycyjną z nowymi danymi. Spowoduje to ponowne utworzenie funkcji – w razie potrzeby widżety emitowane przez tę funkcję zostaną w razie potrzeby odświeżone nowymi danymi. Platforma tworzenia wiadomości pozwala inteligentnie tworzyć tylko te komponenty, które się zmieniły.

Weźmy na przykład 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 element wywołujący aktualizuje wartość clicks. Funkcja Compose wywołuje ponownie lambda z funkcją Text, aby pokazać nową wartość. Ten proces nazywa się rekompozycja. Inne funkcje, które nie zależą od wartości, nie są tworzone ponownie.

Jak już wspomnieliśmy, ponowne utworzenie całego drzewa interfejsu może być kosztowne, co zużywa moc obliczeniową i czas pracy na baterii. Funkcja Utwórz rozwiązuje ten problem dzięki tej inteligentnej zmianie kompozycji.

Zmiana kompozycji to proces ponownego wywoływania funkcji kompozycyjnych po zmianie danych wejściowych. Dzieje się tak, gdy zmienią się dane wejściowe funkcji. Gdy funkcja Compose rekomponuje się na podstawie nowych danych wejściowych, wywołuje tylko funkcje lub lambda, które mogły się zmienić, i pomija pozostałe. Pomijając wszystkie funkcje lub lambda, które nie mają zmienionych parametrów, funkcja tworzenia umożliwia sprawne redagowanie.

Nigdy nie zależy od skutków ubocznych wykonywania funkcji kompozycyjnych, ponieważ zmiana kompozycji funkcji może zostać pominięta. Jeśli to zrobisz, użytkownicy mogą zauważyć w niej dziwne i nieprzewidywalne działanie. Efektem ubocznym jest każda zmiana widoczna w pozostałej części aplikacji. Na przykład te działania to niebezpieczne efekty uboczne:

  • Zapisywanie we właściwości obiektu współdzielonego
  • Aktualizowanie obiektu obserwowalnego w ViewModel
  • Aktualizuję wspólne preferencje

Funkcje kompozycyjne mogą być wykonywane ponownie tak często, jak każda klatka, na przykład podczas renderowania animacji. Funkcje kompozycyjne powinny być szybkie, aby uniknąć zacinania się podczas animacji. Jeśli musisz wykonywać kosztowne operacje, takie jak odczyt ze wspólnych preferencji, wykonaj je w współpracy w tle i przekaż wynik do funkcji kompozycyjnej jako parametr.

Ten kod tworzy na przykład element kompozycyjny, by zaktualizować wartość w zasobniku SharedPreferences. Element kompozycyjny nie powinien odczytywać ani zapisywać danych z preferowanych ustawień. Zamiast tego przenosi odczyt i zapis do instancji ViewModel w korutynie 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 tworzenia:

  • Funkcje kompozycyjne mogą być wykonywane w dowolnej kolejności.
  • Funkcje kompozycyjne mogą być wykonywane równolegle.
  • Zmiana kompozycji powoduje pominięcie jak największej liczby funkcji kompozycyjnych i lambda.
  • Zmiana kompozycji jest optymistyczna i może zostać anulowana.
  • Funkcja kompozycyjna może być uruchamiana dość często, równie często jak każda klatka animacji.

W sekcjach poniżej omówimy, jak tworzyć funkcje kompozycyjne obsługujące zmianę kompozycji. W każdym przypadku sprawdzoną metodą jest dbanie o to, aby funkcje kompozycyjne były szybkie, idempotentne i pozbawione efektów ubocznych.

Funkcje kompozycyjne mogą być wykonywane w dowolnej kolejności

Gdy spojrzysz na kod funkcji kompozycyjnej, możesz założyć, że jest on uruchamiany w takiej kolejności, w jakiej się pojawia. Nie musi to jednak być prawdą. Jeśli funkcja kompozycyjna zawiera wywołania innych funkcji kompozycyjnych, mogą one być uruchamiane w dowolnej kolejności. Funkcja tworzenia umożliwia rozpoznawanie, że niektóre elementy interfejsu mają wyższy priorytet od innych, i rysowanie ich jako pierwsze.

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ą mieć miejsce w dowolnej kolejności. Oznacza to, że nie można na przykład ustawić w funkcji StartScreen() jakiejś zmiennej globalnej (efekt uboczny), aby MiddleScreen() korzystała z tej zmiany. Każda z tych funkcji musi być samodzielna.

Funkcje kompozycyjne mogą być uruchamiane równolegle

Tworzenie może optymalizować kompozycję przez równoczesne uruchamianie funkcji kompozycyjnych. Dzięki temu interfejs Compose może korzystać z wielu rdzeni i uruchamiać funkcje kompozycyjne, które nie są uruchamiane 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ę ViewModel, może ona wywołać ją z kilku wątków jednocześnie.

Aby aplikacja działała prawidłowo, wszystkie funkcje kompozycyjne nie powinny wywoływać skutków ubocznych. Zamiast tego aktywuj efekty uboczne wywołań zwrotnych, takich jak onClick, które zawsze są wykonywane w wątku interfejsu użytkownika.

Wywołanie funkcji kompozycyjnej może wystąpić w innym wątku niż element wywołujący. Oznacza to, że należy unikać kodu, który modyfikuje zmienne w obiekcie lambda kompozycyjnej – zarówno dlatego, że kod nie jest bezpieczny dla wątków, jak i niedopuszczalny efekt uboczny funkcji kompozycyjnej lambda.

Oto przykład elementu kompozycyjnego, który 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 ma efektów ubocznych i przekształca listę wejściową w interfejs. To świetny kod do wyświetlania małej listy. Jeśli jednak funkcja zapisze dane w zmiennej lokalnej, kod ten nie będzie bezpieczny w wątku ani prawidłowy:

@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 pokazuje niewłaściwą liczbę. Z tego powodu takie zapisy nie są obsługiwane w komponencie. Zakazując takiego zapisu, umożliwiamy platformie zmienianie wątków w celu wykonywania funkcji kompozycyjnych lambda.

Zmiana kompozycji powoduje pomijanie tak często, jak to możliwe

Gdy elementy interfejsu użytkownika są nieprawidłowe, funkcja tworzenia wykonywana jest jak najlepiej, aby utworzyć tylko te elementy, które wymagają aktualizacji. Oznacza to, że może pominąć i ponownie uruchomić element kompozycyjny pojedynczego przycisku bez wykonywania żadnego z elementów kompozycyjnych nad lub pod nim w drzewie interfejsu.

Każda funkcja kompozycyjna i lambda mogą się rekomponować same. Oto przykład, który pokazuje, w jaki sposób funkcja 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)
        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ą czynnością wykonywaną podczas zmiany kompozycji. Po zmianie parametru header funkcja tworzenia może przechodzić do lambda Column bez wykonywania żadnego z jego elementów nadrzędnych. A podczas wykonywania Column funkcja tworzenia może pominąć elementy elementu LazyColumn, jeśli parametr names nie uległ zmianie.

Również w przypadku funkcji kompozycyjnych i lambda wykonywanie wszystkich funkcji kompozycyjnych nie powinno zawierać efektów ubocznych. Jeśli chcesz zastosować efekt uboczny, wywołaj go w ramach wywołania zwrotnego.

Zmiana kompozycji jest optymistyczna

Zmiana kompozycji jest uruchamiana za każdym razem, gdy zespół Compose uzna, że parametry elementu kompozycyjnego mogły się zmienić. Zmiana kompozycji jest optymalna, co oznacza, że funkcja tworzenia oczekuje na zakończenie zmiany przed ponowną zmianą parametrów. Jeśli parametr zmieni się przed zakończeniem zmiany kompozycji, funkcja tworzenia może anulować zmianę kompozycji i uruchomić ją ponownie z nowym parametrem.

Po anulowaniu kompozycji funkcja tworzenia usuwa z niej drzewo interfejsu. Jeśli występują jakiekolwiek efekty uboczne zależne od wyświetlanego interfejsu użytkownika, efekt uboczny zostanie zastosowany nawet wtedy, gdy kompozycja została anulowana. Może to prowadzić do niespójności stanu aplikacji.

Upewnij się, że wszystkie funkcje kompozycyjne i lambda są idempotentne i nie występują w nich skutków ubocznych w celu obsługi optymistycznej zmiany kompozycji.

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

W niektórych przypadkach funkcja kompozycyjna może być uruchamiana w przypadku każdej klatki animacji interfejsu. Jeśli funkcja wykonuje kosztowne operacje, takie jak odczyt z pamięci urządzenia, może powodować zacinanie się interfejsu.

Na przykład jeśli widżet próbował odczytać ustawienia urządzenia, mógł odczytać te ustawienia setki razy na sekundę, co może mieć katastrofalny wpływ na działanie aplikacji.

Jeśli funkcja kompozycyjna wymaga danych, powinna zdefiniować parametry tych danych. Następnie możesz przenieść kosztowne zadanie do innego wątku poza kompozycję i przekazać je do funkcji Utwórz za pomocą funkcji mutableStateOf lub LiveData.

Więcej informacji

Więcej informacji o tym, jak myśleć w komponencie i funkcjach kompozycyjnych, znajdziesz w tych dodatkowych materiałach.

Filmy