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 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 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.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Komponenty i układy Material Design
- Efekty uboczne w edytorze
- Podstawowe informacje o układzie tworzenia wiadomości