Jetpack Compose jest oparty 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. Lepsze zaznajomienie się ze stylem Kotlina może pomóc Ci uniknąć tych pułapek.
Argumenty domyślne
Podczas pisania funkcji w Kotlinie możesz określić wartości domyślne argumentów funkcji, które są używane, gdy wywołujący nie przekazuje tych wartości w prosty sposób. Ta funkcja zmniejsza potrzebę stosowania przeciążonych funkcji.
Załóżmy na przykład, że chcesz napisać funkcję, która rysuje kwadrat. Ta funkcja może mieć jeden wymagany parametr sideLength, który określa długość każdej ze stron. Może mieć kilka opcjonalnych parametrów, takich jak thickness, edgeColor itp. Jeśli wywołujący nie określi tych parametrów, funkcja użyje wartości domyślnych. W innych językach możesz użyć kilku 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 wywołujący nie poda wartości argumentu, oznacza to, że chce użyć wartości domyślnej. Ponadto parametry o nazwach znacznie ułatwiają sprawdzanie, co się dzieje. Jeśli w kodzie zobaczysz takie wywołanie funkcji, bez sprawdzenia kodu drawSquare()
możesz nie wiedzieć, co oznaczają parametry:
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ć komponenty, ale nadal możesz łatwo wywołać domyślne działanie. Możesz na przykład utworzyć prosty element tekstowy:
Text(text = "Hello, Android!")
Ten kod ma taki sam efekt jak poniższy, znacznie bardziej szczegółowy kod, w którym więcej parametrów Text
jest ustawionych 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
, wskazujesz, że w przypadku wszystkich pozostałych parametrów chcesz użyć wartości domyślnych. Drugi fragment kodu sugeruje natomiast, że chcesz jawnie ustawić wartości tych innych parametrów, choć są one domyślnymi wartościami funkcji.
Funkcje wyższego rzędu i wyrażenia lambda
Kotlin obsługuje funkcje wyższego rzędu, czyli takie, 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 to funkcja, którą przycisk wywołuje po kliknięciu przez użytkownika:
Button( // ... onClick = myClickFunction ) // ...
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 musisz jej definiować w innym miejscu, aby przekazać ją funkcji wyższego rzędu. Zamiast tego możesz 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 tutaj, łatwiej jest zdefiniować ją w ramach wyrażenia lambda:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Wyrażenia lambda na końcu
Kotlin oferuje specjalną składnię do wywoływania funkcji wyższego rzędu, których ostatni parametr jest funkcją lambda. Jeśli chcesz przekazać wyrażenie lambda jako ten parametr, możesz użyć składni funkcji lambda. Zamiast umieszczać wyrażenie lambda w nawiasach, umieść 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, takich jak funkcja kompozytowa Column()
, to content
, czyli funkcja, która emituje podrzędne elementy interfejsu użytkownika. Załóżmy, że chcesz utworzyć kolumnę zawierającą 3 elementy tekstowe, do których chcesz 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
.
Jeśli jedynym parametrem jest ta ostatnia funkcja lambda, czyli jeśli ostatni parametr jest funkcją lambda i nie przekazujesz żadnych innych parametrów, możesz całkowicie pominąć nawiasy. Załóżmy na przykład, że nie musisz 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 to wyrażenie lambda definiujące podrzędne elementu. Te podrzędne są określone w nawiasach klamrowych po wywołaniu funkcji.
Zakresy i odbiorcy
Niektóre metody i właściwości są dostępne tylko w określonym zakresie. Ograniczony zakres umożliwia oferowanie funkcji tam, gdzie jest to potrzebne, i unikanie przypadkowego używania tej funkcji w nieodpowiednich sytuacjach.
Rozważ przykład użyty w Compose. Gdy wywołasz kompozytywny układ Row
, lambda treści jest automatycznie wywoływana w ramach RowScope
.
Umożliwia to Row
udostępnienie funkcji, które są ważne tylko w ramach Row
.
Przykład poniżej pokazuje, jak funkcja Row
udostępnia wartość dla modyfikatora 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 funkcje lambda mają dostęp do właściwości i funkcji zdefiniowanych w innym miejscu na podstawie deklaracji 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 literalów funkcji z odbiorcą.
Właściwości delegowane
Kotlin obsługuje delegowane właściwości.
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ą kodu takiego jak ten:
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
Przekształcanie klas danych
Jeśli zdefiniujesz klasę danych, możesz łatwo uzyskać dostęp do danych za pomocą deklaracji destrukturyzacji. Na przykład załóżmy, że definiujesz 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 zapewniają wiele innych przydatnych funkcji. Na przykład podczas definiowania klasy danych kompilator automatycznie definiuje przydatne funkcje, takie jak equals()
i copy()
. Więcej informacji znajdziesz w dokumentacji dotyczącej klas danych.
Obiekty pojedyncze
W Kotlinie łatwo zadeklarować klasy singleton, które zawsze mają tylko 1 wystąpieni. Te pojedyncze wartości 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 specyficznych dla danej dziedziny (DSL) za pomocą konstruktorów bezpiecznych pod względem typów. 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 LazyRow
i LazyColumn
.
@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 pod względem typów konstruktory, używając funkcji literalnych z parametrem odbiorczym.
Jeśli weźmiemy na przykład kompozyt Canvas
, zobaczymy, że przyjmuje on jako parametr funkcję z DrawScope
jako odbiornikiem, onDraw: DrawScope.() -> Unit
, co pozwala blokowi kodu wywoływać funkcje członkowskie 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
W Kotlinie coroutines zapewniają obsługę programowania asynchronicznego na poziomie języka. Coroutines może wstrzymać wykonywanie bez blokowania wątków. Interfejs użytkownika oparty na korespondencji jest z zasady 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 łańcuchów 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. Poniżej znajdziesz przykład użycia interfejsu animateScrollTo
API usługi ScrollState
.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // 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 uruchamiają one blok kodu sekwencyjnie. Bieżąca koordynatowa, która wywołuje funkcję zawieszania, zawiesza swoje wykonanie do momentu, aż zwróci się funkcja zawieszania. Dzieje się tak nawet wtedy, gdy funkcja zawieszania przenosi wykonanie do innego CoroutineDispatcher
. W poprzednim przykładzie instrukcja loadData
nie zostanie wykonana, dopóki nie zostanie zwrócona wartość funkcji zawieszania animateScrollTo
.
Aby kod był wykonywany równolegle, trzeba utworzyć nowe coroutine. W przykładzie powyżej, aby równolegle przewijać do góry ekranu i wczytywać dane z viewModel
, potrzebne są 2 korobony.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // 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() } } ) { /* ... */ }
Dzięki nim łatwiej jest łączyć asynchroniczne interfejsy API. W tym przykładzie łączymy modyfikator pointerInput
z interfejsami API animacji, aby animować pozycję elementu, gdy użytkownik kliknie ekran.
@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) ) }
Więcej informacji o korobocjach znajdziesz w artykule Korobocje w Kotlinie na Androida.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Komponenty i układy w materiałach
- Efekty uboczne w edytorze
- Podstawy tworzenia układu