Jetpack Compose jest oparty na języku Kotlin. W niektórych przypadkach Kotlin udostępnia specjalne idiomy, które ułatwiają pisanie dobrego kodu Compose. Jeśli myślisz w innym języku programowania i tłumaczysz go w myślach na Kotlina, możesz nie wykorzystać wszystkich możliwości Compose i mieć trudności ze zrozumieniem kodu napisanego w języku Kotlin. Lepsze poznanie stylu Kotlina może pomóc Ci uniknąć tych pułapek.
Argumenty domyślne
Podczas pisania funkcji w języku Kotlin możesz określić wartości domyślne argumentów funkcji, które będą używane, jeśli wywołujący nie przekaże tych wartości w sposób jawny. 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żdego boku. Może mieć kilka opcjonalnych parametrów, takich jak thickness, edgeColor itp. Jeśli wywołujący ich nie określi, funkcja użyje wartości domyślnych. W innych językach trzeba by było napisać 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 języku Kotlin 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 ) { }
Oprócz tego, że nie musisz pisać wielu zbędnych funkcji, ta funkcja sprawia, że kod jest znacznie czytelniejszy. Jeśli wywołujący nie określi wartości argumentu, oznacza to, że chce użyć wartości domyślnej. Dodatkowo nazwane parametry znacznie ułatwiają zrozumienie, co się dzieje. Jeśli spojrzysz na kod i zobaczysz wywołanie funkcji takie jak to, możesz nie wiedzieć, co oznaczają parametry, bez sprawdzenia kodu drawSquare():
drawSquare(30, 5, Color.Red);
Z kolei ten kod jest samoudokumentowany:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
Większość bibliotek Compose używa argumentów domyślnych. Dobrą praktyką jest robienie tego samego w przypadku funkcji typu „composable”, które piszesz. Dzięki temu funkcje kompozycyjne można dostosowywać, ale nadal łatwo jest wywołać działanie domyślne. Możesz na przykład utworzyć prosty element tekstowy, taki jak ten:
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 bezpośrednio:
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ż sam się dokumentuje. Podając tylko parametr text, wskazujesz, że w przypadku wszystkich pozostałych parametrów chcesz używać wartości domyślnych. Z kolei drugi fragment kodu oznacza, że chcesz jawnie ustawić wartości tych innych parametrów, mimo że ustawione przez Ciebie wartości są domyślnymi wartościami funkcji.
Funkcje wyższego rzędu i wyrażenia lambda
Kotlin obsługuje funkcje wyższego rzędu, czyli funkcje, które otrzymują inne funkcje jako parametry. Compose opiera się na tym podejściu. Na przykład funkcja typu „composable” Button udostępnia parametr lambda onClick. Wartością tego parametru jest funkcja, którą przycisk wywołuje, gdy użytkownik go kliknie:
Button( // ... onClick = myClickFunction ) // ...
Funkcje wyższego rzędu naturalnie łączą się z wyrażeniami lambda, czyli wyrażeniami, które dają funkcję. Jeśli potrzebujesz funkcji tylko raz, nie musisz definiować jej w innym miejscu, aby przekazać ją do funkcji wyższego rzędu. Zamiast tego możesz po prostu zdefiniować funkcję bezpośrednio za pomocą wyrażenia lambda.
W poprzednim przykładzie przyjęliśmy, że zmienna myClickFunction() została zdefiniowana w innym miejscu.
Jeśli jednak używasz tej funkcji tylko tutaj, prościej jest zdefiniować ją w kodzie za pomocą 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 ostatnim parametrem jest lambda. Jeśli chcesz przekazać wyrażenie lambda jako ten parametr, możesz użyć składni lambda na końcu. Zamiast umieszczać wyrażenie lambda w nawiasach, umieszczasz je po nich. To częsta sytuacja w Compose, więc musisz wiedzieć, jak wygląda kod.
Na przykład ostatnim parametrem wszystkich układów, takich jak funkcja typu „composable” Column(), jest content, czyli funkcja, która emituje elementy interfejsu podrzędnego. Załóżmy, że chcesz utworzyć kolumnę zawierającą 3 elementy tekstowe i musisz zastosować formatowanie. Ten kod by działał, ale jest bardzo uciążliwy:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
Parametr content jest ostatnim w sygnaturze funkcji, a jego wartość przekazujemy jako wyrażenie lambda, więc możemy go wyciągnąć poza nawiasy:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
Oba przykłady mają dokładnie takie samo znaczenie. Nawiasy klamrowe definiują wyrażenie lambda przekazywane do parametru content.
Jeśli jedynym przekazywanym parametrem jest końcowa lambda, czyli jeśli ostatni parametr jest lambdą i nie przekazujesz żadnych innych parametrów, możesz całkowicie pominąć nawiasy. Załóżmy na przykład, że nie musisz przekazywać modyfikatora do elementu Column. Możesz napisać kod 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 elementy podrzędne, które 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 pozwala oferować funkcje tam, gdzie są potrzebne, i unikać przypadkowego używania ich tam, gdzie nie są odpowiednie.
Rozważmy przykład użyty w Compose. Gdy wywołujesz funkcję kompozycyjną układu Row, lambda treści jest automatycznie wywoływana w ramach RowScope.
Dzięki temu Row może udostępniać funkcje, które są prawidłowe tylko w ramach Row.
Poniższy przykład pokazuje, jak Row ujawnia wartość dla konkretnego wiersza w przypadku 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, które są wywoływane w zakresie odbiorcy. 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 sekcji Literały funkcji z odbiorcą w dokumentacji Kotlina.
Właściwości przekazane
Kotlin obsługuje właściwości delegowane. Te właściwości są wywoływane tak, jakby były polami, ale ich wartość jest określana dynamicznie przez ocenę wyrażenia. Te właściwości można rozpoznać po składni by:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
Inny kod może uzyskać dostęp do właściwości za pomocą kodu takiego jak ten:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
Gdy wykonywana jest funkcja println(), wywoływana jest funkcja nameGetterFunction(), która zwraca wartość ciągu tekstowego.
Te właściwości delegowane są szczególnie przydatne, gdy pracujesz z właściwościami opartymi na stanie:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Destrukturyzacja klas danych
Jeśli zdefiniujesz klasę danych, możesz uzyskać dostęp do danych za pomocą deklaracji destrukturyzującej. Załóżmy na przykład, ż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 można zobaczyć 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 gdy definiujesz klasę danych, kompilator automatycznie definiuje przydatne funkcje, takie jak equals() i copy(). Więcej informacji znajdziesz w dokumentacji dotyczącej klas danych.
Obiekty singleton
Kotlin umożliwia deklarowanie singletonów, czyli klas, które zawsze mają tylko jedną instancję. Te singletony są deklarowane za pomocą słowa kluczowego object.
Compose często korzysta z takich obiektów. Na przykład MaterialTheme jest zdefiniowany jako obiekt singleton, a właściwości MaterialTheme.colors, shapes i typography zawierają wartości bieżącego motywu.
Bezpieczne typowo kreatory i języki DSL
Kotlin umożliwia tworzenie języków dziedzinowych (DSL) za pomocą kreatorów z zabezpieczeniami wpisywania. Języki DSL umożliwiają tworzenie złożonych hierarchicznych struktur danych w bardziej łatwy w utrzymaniu i czytelny sposób.
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 umożliwia tworzenie bezpiecznych typów za pomocą literałów funkcji z odbiornikiem.
Jeśli weźmiemy jako przykład funkcję kompozycyjną Canvas, przyjmuje ona jako parametr funkcję z DrawScope jako odbiorcą, onDraw: DrawScope.() -> Unit, co pozwala blokowi kodu wywoływać 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 pod względem typów kreatorach i językach DSL znajdziesz w dokumentacji Kotlina.
Współprogramy Kotlin
Korutyny zapewniają obsługę programowania asynchronicznego na poziomie języka w Kotlin. Korutyny mogą zawieszać wykonywanie bez blokowania wątków. Responsywny interfejs użytkownika jest z natury asynchroniczny, a Jetpack Compose rozwiązuje ten problem, wykorzystując współprogramy na poziomie interfejsu API zamiast wywołań zwrotnych.
Jetpack Compose udostępnia interfejsy API, które umożliwiają bezpieczne korzystanie ze współprogramów w warstwie interfejsu.
Funkcja rememberCoroutineScope zwraca obiekt CoroutineScope, za pomocą którego możesz tworzyć współprogramy w modułach obsługi zdarzeń i wywoływać interfejsy API Compose do zawieszania. Zobacz poniższy przykład z użyciem interfejsu API animateScrollTo 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 współprogramy wykonują blok kodu sekwencyjnie. Działająca współprogram, która wywołuje funkcję zawieszenia, zawiesza swoje działanie do momentu, aż funkcja zawieszenia zwróci wynik. Dzieje się tak nawet wtedy, gdy funkcja zawieszenia przenosi wykonywanie do innego elementu CoroutineDispatcher. W poprzednim przykładzie funkcja loadData nie zostanie wykonana, dopóki funkcja zawieszenia animateScrollTo nie zwróci wyniku.
Aby wykonywać kod równocześnie, należy utworzyć nowe współprogramy. W przykładzie powyżej, aby wczytać równolegle przewijanie do góry ekranu i wczytywanie danych z viewModel, potrzebne są 2 współprogramy.
// 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() } } ) { /* ... */ }
Współprogramy ułatwiają łączenie asynchronicznych interfejsów API. W poniższym przykładzie łączymy modyfikator pointerInput z interfejsami Animation API, 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 and animate // in the same block awaitPointerEventScope { val offset = 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 współprogramach znajdziesz w przewodniku Współprogramy Kotlin na Androidzie.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Komponenty i układy Material Design
- Efekty uboczne w Compose
- Podstawowe informacje o układzie Compose