Jetpack Compose è basato su Kotlin. In alcuni casi, Kotlin offre idiomi speciali che semplificano la scrittura di codice di Compose. Se pensi in un altro linguaggio di programmazione e lo traduci mentalmente in Kotlin, è probabile che tu perda alcune delle funzionalità di Compose e potresti avere difficoltà a comprendere il codice Kotlin scritto in modo idiomatico. Acquisire maggiore familiarità con lo stile di Kotlin può aiutarti a evitare questi problemi.
Argomenti predefiniti
Quando scrivi una funzione Kotlin, puoi specificare valori predefiniti per gli argomenti della funzione, utilizzati se chi chiama non li passa esplicitamente. Questa funzionalità riduce la necessità di funzioni sovraccaricate.
Ad esempio, supponiamo che tu voglia scrivere una funzione che disegni un quadrato. Questa funzione potrebbe avere un singolo parametro obbligatorio, sideLength, che specifica la lunghezza di ogni lato. Potrebbe avere diversi parametri facoltativi, come thickness, edgeColor e così via; se il chiamante non li specifica, la funzione utilizza valori predefiniti. In altre lingue, potresti dover scrivere diverse funzioni:
// 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) { }
In Kotlin, puoi scrivere una singola funzione e specificare i valori predefiniti per gli argomenti:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
Oltre a evitare di dover scrivere più funzioni ridondanti, questa funzionalità consente di leggere il codice in modo molto più chiaro. Se chi chiama non specifica un valore per un argomento, significa che è disposto a utilizzare il valore predefinito. Inoltre, i parametri denominati semplificano molto la comprensione di cosa sta accadendo. Se osservi il codice e noti una chiamata di funzione come questa, potresti non
sapere cosa significano i parametri senza controllare il codice drawSquare()
:
drawSquare(30, 5, Color.Red);
Al contrario, questo codice è autodocumentante:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
La maggior parte delle librerie Compose utilizza argomenti predefiniti ed è buona norma fare lo stesso per le funzioni composable che scrivi. Questa prassi rende i componibili personalizzabili, ma rende semplice richiamare il comportamento predefinito. Ad esempio, potresti creare un semplice elemento di testo come questo:
Text(text = "Hello, Android!")
Questo codice ha lo stesso effetto del seguente codice, con molto più dettagliato, in cui
più parametri
Text
sono impostati esplicitamente:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
Non solo il primo snippet di codice è molto più semplice e facile da leggere, ma è anche autodocumentante. Se specifichi solo il parametro text
, dichiari che per tutti gli altri parametri vuoi utilizzare i valori predefiniti. Al contrario, il
secondo snippet implica che vuoi impostare esplicitamente i valori per questi
altri parametri, anche se i valori impostati sono i valori predefiniti per
la funzione.
Funzioni di ordine superiore ed espressioni lambda
Kotlin supporta le funzioni di ordine superiore, ovvero funzioni che ricevono altre funzioni come parametri. Compose si basa su questo approccio. Ad esempio, la funzione componibile Button
fornisce un parametro lambda onClick
. Il valore
di questo parametro è una funzione, che il pulsante chiama quando l'utente fa clic su di esso:
Button( // ... onClick = myClickFunction ) // ...
Le funzioni di ordine superiore si accoppiano naturalmente con le espressioni lambda, espressioni che restituiscono una funzione. Se hai bisogno della funzione una sola volta, non devi
definirla altrove per trasmetterla alla funzione di ordine superiore. Invece, puoi
definire la funzione direttamente con un'espressione lambda. Nell'esempio precedente, l'elemento myClickFunction()
è definito altrove. Tuttavia, se utilizzi solo quella funzione qui, è più semplice definire la funzione in linea con un'espressione lambda:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Lambda finali
Kotlin offre una sintassi speciale per chiamare funzioni di ordine superiore il cui ultimo parametro è una lambda. Se vuoi passare un'espressione lambda come parametro, puoi utilizzare la sintassi lambda finale. Invece di inserire l'espressione lambda tra parentesi, la inserisci dopo. Questa è una situazione comune in Compose, quindi devi conoscere la struttura del codice.
Ad esempio, l'ultimo parametro di tutti i layout, come la funzione composable Column()
, è content
, una funzione che emette gli elementi UI secondari. Supponiamo di voler creare una colonna contenente tre elementi di testo
e di dover applicare una formattazione. Questo codice funzionerebbe, ma è molto complicato:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
Poiché il parametro content
è l'ultimo nella firma della funzione e ne passiamo il valore come espressione lambda, possiamo estrarlo dalle parentesi:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
I due esempi hanno esattamente lo stesso significato. Le parentesi graffe definiscono l'espressione lambda passata al parametro content
.
Infatti, se l'unico parametro che trasmetti è il parametro lambda finale, ovvero se il parametro finale è una lambda e non passi altri parametri, puoi omettere del tutto le parentesi. Ad esempio, supponiamo che non sia necessario passare un modificatore a Column
. Potresti scrivere il codice come segue:
Column { Text("Some text") Text("Some more text") Text("Last text") }
Questa sintassi è abbastanza comune in Compose, in particolare per gli elementi di layout come
Column
. L'ultimo parametro è un'espressione lambda che definisce gli elementi secondari dell'elemento, che vengono specificati tra parentesi graffe dopo la chiamata di funzione.
Ambiti e ricevitori
Alcuni metodi e proprietà sono disponibili solo in un determinato ambito. L'ambito limitato consente di offrire funzionalità dove sono necessarie ed evitare di utilizzarle accidentalmente dove non sono appropriate.
Prendi in considerazione un esempio utilizzato in Compose. Quando chiami il composable di layout Row
, la lambda dei contenuti viene richiamata automaticamente in un RowScope
.
In questo modo, Row
può esporre funzionalità valide solo all'interno di un Row
.
L'esempio seguente mostra come Row
ha esposto un valore specifico per riga per il modificatore 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) ) }
Alcune API accettano le funzioni lambda chiamate nell'ambito del ricevitore. Questi lambda hanno accesso a proprietà e funzioni definite altrove, in base alla dichiarazione del parametro:
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( /*...*/ /* ... ) } )
Per ulteriori informazioni, consulta i valori letterali delle funzioni con ricevitore nella documentazione di Kotlin.
Proprietà delegati
Kotlin supporta le proprietà delegate.
Queste proprietà vengono chiamate come se fossero campi, ma il loro valore viene determinato dinamicamente valutando un'espressione. Puoi riconoscere queste proprietà dall'utilizzo della sintassi by
:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
Un altro codice può accedere alla proprietà con codice come questo:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
Quando println()
viene eseguito, nameGetterFunction()
viene chiamato per restituire il valore
della stringa.
Queste proprietà delegati sono particolarmente utili quando lavori con proprietà supportate da uno stato:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Distruzione delle classi di dati
Se definisci una classe di dati, puoi accedere facilmente ai dati con una dichiarazione di scomposizione. Ad esempio, supponiamo di definire una classe Person
:
data class Person(val name: String, val age: Int)
Se hai un oggetto di questo tipo, puoi accedere ai relativi valori con codice come questo:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
Questo tipo di codice viene spesso utilizzato nelle funzioni di composizione:
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. // ... }
Le classi di dati offrono molte altre funzionalità utili. Ad esempio, quando definisci una classe di dati, il compilatore definisce automaticamente funzioni utili come equals()
e copy()
. Puoi trovare ulteriori informazioni nella documentazione relativa alle classi di dati.
Oggetti singleton
Kotlin semplifica la dichiarazione dei singleton, classi che hanno sempre una sola istanza. Questi oggetti singoli vengono dichiarati con la parola chiave object
.
Compose utilizza spesso questi oggetti. Ad esempio,
MaterialTheme
è
definito come oggetto singleton; le proprietà MaterialTheme.colors
, shapes
e
typography
contengono tutte i valori per il tema corrente.
Costruttori e DSL sicuri per i tipi
Kotlin consente di creare DSL (linguaggi specifici per il dominio) con costruttori sicuri per i tipi. I DSL consentono di creare strutture di dati gerarchiche complesse in modo più gestibile e leggibile.
Jetpack Compose utilizza i DSL per alcune API come
LazyRow
e 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 garantisce costruttori sicuri per i tipi utilizzando
letterali di funzione con ricevente.
Se prendiamo come esempio il composable Canvas
, questo prende come parametro una funzione con DrawScope
come ricevente, onDraw: DrawScope.() -> Unit
, consentendo al blocco di codice di chiamare le funzioni membro definite in 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) } } }
Scopri di più sui DSL e sui builder sicuri per i tipi nella documentazione di Kotlin.
Coroutine Kotlin
Le coroutine offrono il supporto della programmazione asincrona a livello di linguaggio in Kotlin. Le coroutine possono sospendere l'esecuzione senza bloccare i thread. Una UI reattiva è intrinsecamente asincrona e Jetpack Compose risolve questo problema adottando le coroutine a livello di API invece di utilizzare i callback.
Jetpack Compose offre API che rendono sicuro l'utilizzo delle coroutine all'interno del livello dell'interfaccia utente.
La funzione rememberCoroutineScope
restituisce un CoroutineScope
con cui puoi creare coroutine nei gestori di eventi e richiamare le API di sospensione Compose. Guarda l'esempio seguente che utilizza l'API animateScrollTo
di 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() } } ) { /* ... */ }
Per impostazione predefinita, le coroutine eseguono il blocco di codice sequenzialmente. Una coroutine in esecuzione che chiama una funzione di sospensione sospende la sua esecuzione fino al ritorno della funzione di sospensione. Questo è vero anche se la funzione di sospensione sposta l'esecuzione in un CoroutineDispatcher
diverso. Nell'esempio precedente,
loadData
non verrà eseguito finché non viene restituita la funzione di sospensione animateScrollTo
.
Per eseguire il codice in modo concorrente, è necessario creare nuove coroutine. Nell'esempio precedente, per parallelizzare lo scorrimento fino alla parte superiore dello schermo e il caricamento dei dati da viewModel
, sono necessarie due coroutine.
// 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() } } ) { /* ... */ }
Le coroutine semplificano la combinazione di API asincrone. Nel seguente
esempio, combiniamo il modificatore pointerInput
con le API di animazione per
animare la posizione di un elemento quando l'utente tocca lo schermo.
@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) ) }
Per scoprire di più sulle coroutine, consulta la guida sulle coroutine Kotlin su Android.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Componenti e layout di Material
- Effetti collaterali in Componi
- Nozioni di base sul layout di composizione