Kotlin dla Jetpack Compose

Jetpack Compose opiera się na Kotlinie. W niektórych przypadkach Kotlin udostępnia specjalne idiomy, które ułatwiają tworzenie dobrego kodu Compose. Jeśli myślisz w innym języku programowania i mentalnie tłumaczysz ten język na Kotlin, prawdopodobnie nie wykorzystasz w pełni zalet Compose i może Ci być trudno zrozumieć kod Kotlina napisany w stylu idiomatycznym. Przybranie większej liczby znajomość stylu Kotlina może pomóc uniknąć tych pułapek.

Argumenty domyślne

Przy pisaniu funkcji Kotlin możesz określić wartości domyślne dla funkcji , używane, jeśli element wywołujący nie przekazuje tych wartości wprost. Ta funkcja zmniejsza potrzebę stosowania przeciążonych funkcji.

Załóżmy na przykład, że chcesz napisać funkcję, która rysuje kwadrat. Ten funkcja może mieć pojedynczy wymagany parametr sideLength, który określa długość z każdej strony. Może mieć kilka parametrów opcjonalnych, takich jak grubość, edgeColor itp; jeśli rozmówca ich nie określi, zostanie funkcja używa wartości domyślnych. W przypadku innych języków możesz oczekiwać, że kilka funkcji:

// We don't need to do this in Kotlin!
void drawSquare(int sideLength) { }

void drawSquare(int sideLength, int thickness) { }

void drawSquare(int sideLength, int thickness, Color edgeColor) { }

W Kotlinie możesz napisać jedną funkcję i określić domyślne wartości argumentów:

fun drawSquare(
    sideLength: Int,
    thickness: Int = 2,
    edgeColor: Color = Color.Black
) {
}

Dzięki tej funkcji nie musisz pisać wielu zbędnych funkcji, a Twój kod będzie znacznie czytelniejszy. Jeśli rozmówca nie określi dla argumentu, co wskazuje, że klient jest skłonny użyć domyślnej . Nazwane parametry znacznie ułatwiają też sprawdzanie, włącz. Jeśli spojrzysz na kod i zauważysz takie wywołanie funkcji, możesz nie możesz poznać znaczenie poszczególnych parametrów bez sprawdzania kodu drawSquare():

drawSquare(30, 5, Color.Red);

Ten kod zawiera swój własny opis:

drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)

Większość bibliotek Compose używa argumentów domyślnych i warto robić to samo w przypadku funkcji kompozytowych, które piszesz. Dzięki temu możesz dostosowywać swoje komponenty, ale nadal możesz łatwo wywołać domyślne zachowanie. Na przykład można utworzyć prosty element tekstowy w taki sposób:

Text(text = "Hello, Android!")

Kod ten będzie miał taki sam efekt jak poniższy, dużo bardziej szczegółowy, więcej Text są ustawiane jawnie:

Text(
    text = "Hello, Android!",
    color = Color.Unspecified,
    fontSize = TextUnit.Unspecified,
    letterSpacing = TextUnit.Unspecified,
    overflow = TextOverflow.Clip
)

Pierwszy fragment kodu jest nie tylko znacznie prostszy i łatwiejszy do odczytania, ale też samodokumentujący. Określając tylko parametr text, dokumentujesz, że dla dla pozostałych parametrów, użyj wartości domyślnych. Z kolei atrybut wskazuje, że chcesz wprost ustawić wartości tych innych parametrów, choć ustawione przez Ciebie wartości są wartościami domyślnymi do funkcji.

Funkcje wyższego rzędu i wyrażenia lambda

Kotlin obsługuje wyższe rzędy funkcji, które otrzymują inne funkcje jako parametry. Narzędzie Compose opiera się na tym podejściu. Na przykład funkcja składana Button udostępnia parametr lambda onClick. Wartość tego parametru jest funkcją, która jest wywoływana przez przycisk po kliknięciu go przez użytkownika:

Button(
    // ...
    
)
// ...

Funkcje wyższego rzędu naturalnie łączą się z wyrażeniami lambda, które są obliczane jako funkcje. Jeśli funkcja jest potrzebna tylko raz, nie trzeba zdefiniować w innym miejscu i przekazać do funkcji wyższego rzędu. Zamiast tego możesz: od razu zdefiniować funkcję za pomocą wyrażenia lambda. W poprzednim przykładzie zakładamy, że myClickFunction() jest zdefiniowany gdzie indziej. Jeśli jednak używasz tej funkcji tylko w tym miejscu, łatwiej jest zdefiniować ją bezpośrednio za pomocą wyrażenia lambda:

Button(
    // ...
    
        // do something
        // do something else
    }
) { /* ... */ }

Wyniki lambda

Kotlin oferuje specjalną składnię do wywoływania funkcji wyższego rzędu, których ostatni parametr jest funkcją lambda. Jeśli chcesz przekazywać wyrażenie lambda w taki sposób, można użyć lambda śledzenia . Zamiast umieszczać wyrażenie lambda w nawiasach, umieszczasz je na końcu. Jest to częsta sytuacja w Compose, więc musisz wiedzieć, jak wygląda kod.

Na przykład ostatni parametr wszystkich układów, taki jak Column() funkcja kompozycyjna to content, funkcja emitująca podrzędny interfejs użytkownika . Załóżmy, że chcesz utworzyć kolumnę zawierającą trzy elementy tekstowe, i musisz zastosować formatowanie. Ten kod zadziała, ale jest bardzo kłopotliwy:

Column(
    modifier = Modifier.padding(16.dp),
    content = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

Ponieważ parametr content jest ostatnim w podpisie funkcji, a jego wartość jest wyrażeniem lambda, możemy go wyjąć z nawiasów:

Column(modifier = Modifier.padding(16.dp)) {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

Oba przykłady mają dokładnie to samo znaczenie. Zwięzły określa wyrażenie lambda przekazywane do parametru content.

W rzeczywistości, jeśli przekazywanym jedynym parametrem jest końcowa lambda, czyli jeśli końcowym parametrem jest lambda i nie przekazujesz żadnych innych parametrów – możesz całkowicie pominąć nawiasy. Załóżmy na przykład, że chcesz nie trzeba było przekazywać modyfikatora do funkcji Column. Kod możesz napisać w ten sposób:

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

Ta składnia jest dość powszechna w Compose, zwłaszcza w przypadku elementów układu, takich jak Column. Ostatni parametr jest wyrażeniem lambda określającym dzieci, które po wywołaniu funkcji określa się w nawiasach klamrowych.

Zakresy i odbiorniki

Niektóre metody i właściwości są dostępne tylko w określonym zakresie. Ograniczone pozwala oferować funkcje tam, gdzie są potrzebne, i uniknąć przypadkowego używanie jej tam, gdzie jest to niewłaściwe.

Zobacz przykład używany w funkcji Compose. Gdy wywołasz kompozytywny układ Row, lambda treści jest automatycznie wywoływana w ramach RowScope. Dzięki temu Row może udostępniać funkcje, które działają tylko w obrębie Row. Przykład poniżej pokazuje, jak funkcja Row wyświetla wartość związaną z wierszem modyfikator align:

Row {
    Text(
        text = "Hello world",
        // This Text is inside a RowScope so it has access to
        // Alignment.CenterVertically but not to
        // Alignment.CenterHorizontally, which would be available
        // in a ColumnScope.
        modifier = Modifier.align(Alignment.CenterVertically)
    )
}

Niektóre interfejsy API akceptują funkcje lambda wywoływane w zakresie odbiornika. Te lambda mają dostęp do właściwości i funkcji zdefiniowanych w innym miejscu na podstawie deklaracja parametru:

Box(
    modifier = Modifier.drawBehind {
        // This method accepts a lambda of type DrawScope.() -> Unit
        // therefore in this lambda we can access properties and functions
        // available from DrawScope, such as the `drawRectangle` function.
        drawRect(
            /*...*/
            /* ...
        )
    }
)

Więcej informacji znajdziesz w dokumentacji Kotlina na temat literałów funkcji z odbiorcą.

Usługi delegowane

Kotlin popiera delegowane usługi. Te właściwości są wywoływane tak, jakby były polami, ale ich wartość jest określana dynamicznie przez wykonanie wyrażenia. Możesz rozpoznać te właściwości po użyciu składni by:

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

Inny kod może uzyskać dostęp do usługi za pomocą takiego kodu:

val myDC = DelegatingClass()
println("The name property is: " + myDC.name)

Gdy funkcja println() zostanie wykonana, wywołana zostanie funkcja nameGetterFunction(), która zwróci wartość ciągu znaków.

Te usługi delegowane są szczególnie przydatne, gdy pracujesz z usługami obsługiwanymi przez stan:

var showDialog by remember { mutableStateOf(false) }

// Updating the var automatically triggers a state change
showDialog = true

Niszczenie klas danych

Jeśli zdefiniujesz klasę danych, możesz łatwo uzyskać dostęp do danych za pomocą deklaracji destrukturyzacji. Dla: Załóżmy, że zdefiniujesz klasę Person:

data class Person(val name: String, val age: Int)

Jeśli masz obiekt tego typu, możesz uzyskać dostęp do jego wartości za pomocą kodu takiego jak ten:

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

Taki kod często pojawia się w funkcjach Compose:

Row {

    val (image, title, subtitle) = createRefs()

    // The `createRefs` function returns a data object;
    // the first three components are extracted into the
    // image, title, and subtitle variables.

    // ...
}

Klasy danych mają wiele innych przydatnych funkcji. Jeśli na przykład klasy danych, kompilator automatycznie zdefiniuje przydatne funkcje, equals() i copy(). Więcej informacji można znaleźć w danych zajęcia.

Obiekty Singleton

W Kotlinie łatwo zadeklarować klasy singleton, które zawsze mają tylko 1 wystąpieni. Te singletony są deklarowane za pomocą słowa kluczowego object. Compose często korzysta z takich obiektów. Na przykład obiekt MaterialTheme jest zdefiniowany jako obiekt pojedynczy; właściwości MaterialTheme.colors, shapes i typography zawierają wartości bieżącego motywu.

Typowo bezpieczne kreatory i języki opisu danych

Kotlin umożliwia tworzenie języków właściwych dla domeny (DSL). za pomocą konstruktorów bezpiecznych do pisania. Języki DSL umożliwiają tworzenie złożonych hierarchicznych struktur danych w bardziej czytelnej i łatwej w utrzymaniu formie.

Jetpack Compose używa języków DSL w przypadku niektórych interfejsów API, takich jak LazyRowLazyColumn.

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        // Add a single item as a header
        item {
            Text("Message List")
        }

        // Add list of messages
        items(messages) { message ->
            Message(message)
        }
    }
}

Kotlin gwarantuje bezpieczne typy konstruktorów, literały funkcji z odbiornikiem. Jeśli weźmiemy Canvas jako „composable”, przyjmuje jako parametr funkcję z DrawScope jako odbiornik (onDraw: DrawScope.() -> Unit), co umożliwia blokowi kodu wywołuje funkcje składowe zdefiniowane w DrawScope.

Canvas(Modifier.size(120.dp)) {
    // Draw grey background, drawRect function is provided by the receiver
    drawRect(color = Color.Gray)

    // Inset content by 10 pixels on the left/right sides
    // and 12 by the top/bottom
    inset(10.0f, 12.0f) {
        val quadrantSize = size / 2.0f

        // Draw a rectangle within the inset bounds
        drawRect(
            size = quadrantSize,
            color = Color.Red
        )

        rotate(45.0f) {
            drawRect(size = quadrantSize, color = Color.Blue)
        }
    }
}

Więcej informacji o bezpiecznych konstruktorach i językach DSL znajdziesz w dokumentacji Kotlina.

współprogramy Kotlina

System Coroutines oferuje wsparcie w zakresie programowania asynchronicznego na poziomie języka Kotlin. Koutyny mogą zawieszać wykonywanie bez blokowania wątków. Interfejs użytkownika oparty na kodzie responsywnym jest z natury asynchroniczny, a Jetpack Compose rozwiązuje ten problem, stosując coroutine na poziomie interfejsu API zamiast wywołań zwrotnych.

Jetpack Compose udostępnia interfejsy API, które umożliwiają bezpieczne używanie coroutine w warstwie interfejsu użytkownika. Funkcja rememberCoroutineScope zwraca obiekt CoroutineScope, za pomocą którego możesz tworzyć łańcuchy w metodach obsługi zdarzeń i wywoływać zawieszone interfejsy Compose. Zobacz ten przykład, używając ScrollState Interfejs API animateScrollTo.

// Create a CoroutineScope that follows this composable's lifecycle
val composableScope = rememberCoroutineScope()
Button(
    // ...
    
        // Create a new coroutine that scrolls to the top of the list
        // and call the ViewModel to load data
        composableScope.launch {
            scrollState.animateScrollTo(0) // This is a suspend function
            viewModel.loadData()
        }
    }
) { /* ... */ }

Domyślnie koronyty wykonują blok kodu sekwencyjnie. Bieżąca koperta wywołująca funkcję zawieszania wstrzymuje swoje wykonanie do czasu, aż zwróci się funkcja zawieszania. Dzieje się tak nawet wtedy, gdy funkcja zawieszenia przenosi wykonanie do innego CoroutineDispatcher. W poprzednim przykładzie Funkcja loadData nie zostanie wykonana, dopóki funkcja zawieszenia nie zostanie wykonana animateScrollTo „powrót karetki”.

Aby wykonywać kod równolegle, musisz utworzyć nowe coroutine. W tym przykładzie powyżej, aby przewijać równolegle do góry ekranu i wczytywać dane viewModel, wymagane są 2 współrzędne.

// Create a CoroutineScope that follows this composable's lifecycle
val composableScope = rememberCoroutineScope()
Button( // ...
    
        // Scroll to the top and load data in parallel by creating a new
        // coroutine per independent work to do
        composableScope.launch {
            scrollState.animateScrollTo(0)
        }
        composableScope.launch {
            viewModel.loadData()
        }
    }
) { /* ... */ }

Koputy ułatwiają łączenie asynchronicznych interfejsów API. W następujących łączymy modyfikator pointerInput z interfejsami API animacji, animowanie pozycji elementu po kliknięciu przez użytkownika ekranu;

@Composable
fun MoveBoxWhereTapped() {
    // Creates an `Animatable` to animate Offset and `remember` it.
    val animatedOffset = remember {
        Animatable(Offset(0f, 0f), Offset.VectorConverter)
    }

    Box(
        // The pointerInput modifier takes a suspend block of code
        Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                // Create a new CoroutineScope to be able to create new
                // coroutines inside a suspend function
                coroutineScope {
                    while (true) {
                        // Wait for the user to tap on the screen
                        val offset = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        // Launch a new coroutine to asynchronously animate to
                        // where the user tapped on the screen
                        launch {
                            // Animate to the pressed position
                            animatedOffset.animateTo(offset)
                        }
                    }
                }
            }
    ) {
        Text("Tap anywhere", Modifier.align(Alignment.Center))
        Box(
            Modifier
                .offset {
                    // Use the animated offset as the offset of this Box
                    IntOffset(
                        animatedOffset.value.x.roundToInt(),
                        animatedOffset.value.y.roundToInt()
                    )
                }
                .size(40.dp)
                .background(Color(0xff3c1361), CircleShape)
        )
    }

Aby dowiedzieć się więcej o Korutynach, sprawdź Przewodnik Kotlin Coroutines na Androidzie.