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 mentalnie tłumaczysz go na Kotlin, prawdopodobnie nie wykorzystasz w pełni możliwości Compose i możesz mieć trudności ze zrozumieniem kodu Kotlin napisanego w idiomatyczny sposób. Lepsze poznanie stylu 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 będą używane, jeśli wywołujący nie przekaże tych wartości. 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ć 1 wymagany parametr, sideLength, określający długość każdego boku. Może mieć kilka parametrów opcjonalnych, takich jak thickness, edgeColor itd. Jeśli wywołujący nie określi tych parametrów, the function użyje wartości domyślnych. W innych językach prawdopodobnie trzeba by 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 Kotlinie możesz napisać jedną funkcję i określić wartości domyślne argumentów:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
Ta funkcja nie tylko pozwala uniknąć pisania wielu zbędnych funkcji, ale też sprawia, że kod jest znacznie bardziej czytelny. Jeśli wywołujący nie określi wartości argumentu, oznacza to, że chce użyć wartości domyślnej. Ponadto nazwane parametry znacznie ułatwiają zrozumienie, co się dzieje. Jeśli spojrzysz na kod i zobaczysz wywołanie funkcji w ten sposób, możesz nie wiedzieć, co oznaczają parametry, bez sprawdzenia kodu drawSquare():
drawSquare(30, 5, Color.Red);
Natomiast ten kod jest samokomentujący:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
Większość bibliotek Compose używa argumentów domyślnych. Dobrą praktyką jest stosowanie tej samej zasady w przypadku funkcji kompozycyjnych, które piszesz. Dzięki temu Twoje funkcje kompozycyjne są konfigurowalne, ale domyślne działanie jest nadal proste do wywołania. Możesz na przykład utworzyć prosty element tekstowy w ten sposób:
Text(text = "Hello, Android!")
Ten kod ma taki sam efekt jak ten bardziej rozbudowany kod, w którym
więcej
Text
parametrów 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ż samokomentujący. Określając tylko parametr text, dokumentujesz, że w przypadku wszystkich innych parametrów chcesz użyć wartości domyślnych. Natomiast drugi fragment sugeruje, że chcesz jawnie ustawić wartości tych innych parametrów, chociaż ustawione wartości są wartościami domyślnymi funkcji.
Funkcje wyższego rzędu i wyrażenia lambda
Kotlin obsługuje funkcje wyższego rzędu , czyli funkcje, które przyjmują inne funkcje jako parametry. Compose opiera się na tym podejściu. Na
przykład funkcja
Button
kompozycyjna 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 są obliczane jako funkcja. Jeśli potrzebujesz funkcji tylko raz, nie musisz jej definiować w innym miejscu, aby przekazać ją do funkcji wyższego rzędu. Zamiast tego możesz po prostu zdefiniować funkcję w tym miejscu za pomocą wyrażenia lambda. W poprzednim przykładzie zakłada się, że funkcja myClickFunction() jest zdefiniowana w innym miejscu. Jeśli jednak używasz tej funkcji tylko tutaj, prościej jest zdefiniować ją w tekście 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 wyrażenia lambda na końcu. Zamiast umieszczać wyrażenie lambda w nawiasach, umieszczasz je później. W Compose jest to częsta sytuacja, dlatego musisz wiedzieć, jak wygląda kod.
Na przykład ostatnim parametrem wszystkich układów, takim jak funkcja kompozycyjna
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ć do nich formatowanie. Ten kod będzie działać, ale jest bardzo niewygodny:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
Ponieważ parametr content jest ostatnim w sygnaturze funkcji, a jego wartość przekazujemy jako wyrażenie lambda, możemy go wyciągnąć 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. Nawiasy klamrowe definiują wyrażenie lambda, które jest przekazywane do parametru content.
Jeśli jedynym przekazywanym parametrem jest wyrażenie lambda na końcu, czyli jeśli ostatni parametr jest wyrażeniem lambda i nie przekazujesz żadnych innych parametrów, możesz pominąć nawiasy. Załóżmy na przykład, że nie musisz przekazywać modyfikatora do 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 elementu, a te elementy podrzędne są określone w nawiasach klamrowych po wywołaniu funkcji.
Zakresy i odbiorniki
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 w miejscach, w których nie są odpowiednie.
Rozważmy przykład używany w Compose. Gdy wywołujesz funkcję kompozycyjną układu Row, lambda treści jest automatycznie wywoływana w RowScope.
Dzięki temu Row może udostępniać funkcje, które są prawidłowe tylko w Row.
Poniższy przykład pokazuje, jak Row udostępnił wartość specyficzną dla 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ą wyrażenia lambda, które są wywoływane w zakresie odbiornika. Te wyrażenia 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 w artykule o literałach funkcji z odbiornikiem.
Właściwości delegowane
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 obliczenie wyrażenia. Te właściwości można rozpoznać po użyciu składni by:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
Inny kod może uzyskać dostęp do właściwości za pomocą takiego kodu:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
Gdy wykonywana jest funkcja println(), wywoływana jest funkcja nameGetterFunction(), aby zwrócić wartość ciągu.
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
remember
Destrukturyzacja klas danych
Jeśli zdefiniujesz klasę
danych, możesz łatwo
uzyskać dostęp do danych za pomocą deklaracji
destrukturyzacji. 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ą takiego kodu:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
Ten rodzaj kodu często występuje 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 udostępniają 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 klas
danych.
Obiekty singleton
Kotlin ułatwia deklarowanie singletonów, czyli klas, które zawsze mają tylko jedną i
jedyną 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. Właściwości MaterialTheme.colors, shapes i
typography zawierają wartości bieżącego motywu.
Budowniczowie z bezpieczeństwem typów i DSL
Kotlin umożliwia tworzenie języków specyficznych dla domeny (DSL) za pomocą budowniczych z bezpieczeństwem typów. DSL umożliwiają tworzenie złożonych hierarchicznych struktur danych w sposób bardziej czytelny i łatwiejszy w utrzymaniu.
Jetpack Compose używa 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 budowniczych z bezpieczeństwem typów za pomocą
literałów funkcji z odbiornikiem.
Jeśli weźmiemy na przykład funkcję kompozycyjną Canvas, przyjmuje ona jako parametr funkcję z
DrawScope
jako odbiornikiem, 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 budowniczych z bezpieczeństwem typów i DSL znajdziesz w dokumentacji Kotlina.
Współprogramy Kotlin
Współprogramy oferują obsługę programowania asynchronicznego na poziomie języka w Kotlinie. Współprogramy mogą wstrzymywać wykonywanie bez blokowania wątków. Responsywny interfejs użytkownika jest z natury asynchroniczny, a Jetpack Compose rozwiązuje ten problem, stosują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 użytkownika.
Funkcja rememberCoroutineScope
zwraca CoroutineScope, za pomocą którego możesz tworzyć współprogramy w procedurach obsługi zdarzeń i wywoływać
interfejsy API wstrzymania Compose. Poniżej znajdziesz przykład użycia interfejsu API
ScrollState's
animateScrollTo.
// 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ący współprogram, który wywołuje funkcję wstrzymania, wstrzymuje swoje wykonanie do momentu, aż funkcja wstrzymania zwróci wartość. Dzieje się tak nawet wtedy, gdy funkcja wstrzymania przenosi wykonanie do innego CoroutineDispatcher. W poprzednim przykładzie funkcja loadData nie zostanie wykonana, dopóki funkcja wstrzymania animateScrollTo nie zwróci wartości.
Aby wykonywać kod współbieżnie, trzeba utworzyć nowe współprogramy. W powyższym przykładzie, aby zrównoleglić 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 API animacji, aby animować położenie elementu, gdy użytkownik dotknie 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 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
- Efekty uboczne w Compose
- Podstawy układu Compose