Jetpack Compose è basato su Kotlin. In alcuni casi, Kotlin fornisce espressioni speciali che semplificano la scrittura di codice Compose di qualità. Se pensi in un altro linguaggio di programmazione e lo traduci mentalmente in Kotlin, è probabile che ti perderai alcuni dei punti di forza 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 queste insidie.
Argomenti predefiniti
Quando scrivi una funzione Kotlin, puoi specificare valori predefiniti per gli argomenti della funzione, utilizzati se il chiamante non passa esplicitamente questi valori. Questa funzionalità riduce la necessità di funzioni di overload.
Supponiamo, ad esempio, di voler 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 i valori predefiniti. In altri linguaggi, potresti aspettarti di 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 evitarti di dover scrivere più funzioni ridondanti, questa funzionalità rende il codice molto più chiaro da leggere. Se il chiamante non specifica un valore per un argomento, significa che è disposto a utilizzare il valore predefinito. Inoltre, i parametri denominati semplificano notevolmente la comprensione di ciò che sta succedendo. Se esamini il codice e vedi 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 è autoesplicativo:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
La maggior parte delle librerie Compose utilizza argomenti predefiniti ed è una buona pratica fare lo stesso per le funzioni componibili che scrivi. Questa pratica rende i tuoi componibili personalizzabili, ma ne semplifica comunque l'invocazione del 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, molto più dettagliato, in cui
vengono impostati esplicitamente più parametri Text:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
Il primo snippet di codice non è solo molto più semplice e facile da leggere, ma è anche autoesplicativo. Specificando solo il parametro text, indichi 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 le funzioni che
ricevono altre funzioni come parametri. Compose si basa su questo approccio. Ad
esempio, la Button funzione componibile fornisce un onClick
parametro lambda. 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 abbinano naturalmente alle espressioni lambda, ovvero le espressioni
che restituiscono una funzione. Se hai bisogno della funzione una sola volta, non devi definirla altrove per passarla alla funzione di ordine superiore. In alternativa, puoi definire la funzione direttamente con un'espressione lambda.
L'esempio precedente presuppone che myClickFunction() sia definita altrove.
Tuttavia, se utilizzi questa funzione solo qui, è più semplice definirla in linea con un'espressione lambda:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Lambda finali
Kotlin offre una sintassi speciale per chiamare le funzioni di ordine superiore il cui ultimo parametro è una lambda. Se vuoi passare un'espressione lambda come quel 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 l'aspetto del codice.
Ad esempio, l'ultimo parametro di tutti i layout, come la
Column() funzione componibile, è content, una funzione che
emette gli elementi dell'UI secondari. Supponiamo di voler creare una colonna contenente tre elementi di testo e di dover applicare una formattazione. Questo codice funzionerebbe, ma è molto macchinoso:
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 stiamo passando il suo 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 stai passando è la lambda finale, ovvero se il parametro finale è una lambda e non stai passando altri parametri, puoi omettere completamente le parentesi. Supponiamo, ad esempio, di non dover passare un modificatore a Column. Potresti scrivere il codice in questo modo:
Column { Text("Some text") Text("Some more text") Text("Last text") }
Questa sintassi è abbastanza comune in Compose, soprattutto 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 ti consente di offrire funzionalità dove sono necessarie ed evitare di utilizzarle accidentalmente dove non sono appropriate.
Considera un esempio utilizzato in Compose. Quando chiami il componibile di layout Row, la lambda dei contenuti viene richiamata automaticamente all'interno di 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 della 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 lambda chiamate nell' ambito del ricevitore. Queste 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 saperne di più, consulta i valori letterali delle funzioni con ricevitore in ella documentazione di Kotlin.
Proprietà delegate
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() // ... }
Altro codice può accedere alla proprietà con codice simile a questo:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
Quando viene eseguito println(), viene chiamata nameGetterFunction() per restituire il valore della stringa.
Queste proprietà delegate sono particolarmente utili quando lavori con proprietà supportate dallo stato:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Destrutturazione delle classi di dati
Se definisci una classe di dati, puoi accedere ai dati con una
dichiarazione di destrutturazione. Supponiamo, ad esempio, di definire una classe Person:
data class Person(val name: String, val age: Int)
Se hai un oggetto di questo tipo, puoi accedere ai suoi valori con codice simile a questo:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
Spesso vedrai questo tipo di codice nelle funzioni 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. // ... }
Le classi di dati forniscono molte altre funzionalità utili. Ad esempio, quando definisci una classe di dati, il compilatore definisce automaticamente funzioni utili come equals() e copy(). Puoi trovare maggiori informazioni nella
documentazione sulle classi di dati.
Oggetti singleton
Kotlin ti consente di dichiarare singleton, ovvero classi che hanno sempre una e una sola
istanza. Questi singleton vengono dichiarati con la object parola chiave.
Compose utilizza spesso questi oggetti. Ad esempio,
MaterialTheme è definito come oggetto singleton; le proprietà
MaterialTheme.colors, shapes e typography contengono tutte i
valori del tema corrente.
Generatori con sicurezza dei tipi e DSL
Kotlin consente di creare linguaggi specifici per il dominio (DSL) con generatori con sicurezza dei tipi. Le DSL consentono di creare strutture di dati gerarchiche complesse in modo più gestibile e leggibile.
Jetpack Compose utilizza le 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 consente di utilizzare generatori con sicurezza dei tipi utilizzando
valori letterali delle funzioni con ricevitore.
Se prendiamo come esempio il componibile Canvas, questo accetta come
parametro una funzione con DrawScope come ricevitore,
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 generatori con sicurezza dei tipi e sulle DSL nella documentazione di Kotlin.
Coroutine Kotlin
Le coroutine offrono supporto per la programmazione asincrona a livello di linguaggio in Kotlin. Le coroutine possono sospendere l'esecuzione senza bloccare i thread. Un'UI reattiva è intrinsecamente asincrona e Jetpack Compose risolve questo problema adottando le coroutine a livello di API anziché utilizzare i callback.
Jetpack Compose offre API che rendono sicuro l'utilizzo delle coroutine all'interno del livello UI.
La rememberCoroutineScope funzione restituisce
un CoroutineScope con cui puoi creare coroutine
nei gestori di eventi e chiamare le API di sospensione di Compose. Vedi l'esempio seguente
che utilizza l'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() } } ) { /* ... */ }
Per impostazione predefinita, le coroutine eseguono il blocco di codice in sequenza. Una coroutine in esecuzione che chiama una funzione di sospensione sospende la sua esecuzione finché la funzione di sospensione non restituisce un valore. Ciò vale anche se la funzione di sospensione sposta l'esecuzione su un CoroutineDispatcher diverso. Nell'esempio precedente, loadData non verrà eseguito finché la funzione di sospensione animateScrollTo non restituisce un valore.
Per eseguire il codice contemporaneamente, è necessario creare nuove coroutine. Nell'esempio precedente, per parallelizzare lo scorrimento verso la 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. Nell'esempio seguente, 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 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) ) }
Per saperne di più sulle coroutine, consulta la guida Coroutine Kotlin su Android.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Componenti e layout Material
- Effetti collaterali in Compose
- Nozioni di base sul layout di Compose