Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Kotlin para Jetpack Compose

Jetpack Compose se basa en Kotlin. En algunos casos, Kotlin proporciona expresiones idiomáticas especiales que facilitan la escritura de código útil de Compose. Si piensas en otro lenguaje de programación y lo traduces mentalmente a Kotlin, es probable que pierdas parte de la solidez de Compose y que te resulte difícil comprender el código Kotlin escrito con expresiones idiomáticas. Familiarizarte con el estilo de Kotlin te puede ayudar a evitar esos problemas.

Argumentos predeterminados

Cuando escribes una función de Kotlin, puedes especificar valores predeterminados para argumentos de funciones, que se van a usar si el llamador no pasa valores explícitos. Eso reduce la necesidad de que se sobrecarguen las funciones.

Por ejemplo, supongamos que deseas escribir una función que dibuje un cuadrado. Esa función podría tener un solo parámetro obligatorio, lado, que especifique la longitud de cada lado. Podría tener varios parámetros opcionales, como espesor, color del borde, relleno, etcétera. Si el llamador no los especifica, la función usa valores predeterminados. En otros lenguajes, es posible que debas escribir varias funciones:

// We don't need to do this in Kotlin!
void drawSquare(int side) {...}
void drawSquare(int side, int thickness) {...}
void drawSquare(int side, int thickness, Color edgeColor ) {...}
// …

En Kotlin, puedes escribir una sola función y especificar los valores predeterminados para los argumentos:

fun drawSquare(
    side: Int, thickness: Int = 2,
    edgeColor: Color = Color.Black,
    fill: Color = Color.White
) { ... }

Además de evitar tener que escribir varias funciones redundantes, esta característica facilita mucho la lectura del código. Si el llamador no especifica un valor para un argumento, significa que está dispuesto a usar el valor predeterminado. Además, los parámetros con nombre permiten ver mucho mejor lo que sucede. Si observas el código y ves una llamada a una función como esta, es posible que no sepas qué significan los parámetros sin verificar el código drawSquare():

drawSquare(30, 5, Color.Red);

Por el contrario, se autodocumenta este código:

drawSquare(side = 30, thickness = 5, edgeColor = Color.Red)

La mayoría de las bibliotecas de Compose usan argumentos predeterminados, y es recomendable que hagas lo mismo para las funciones que escribas y que admiten composición. Eso te permite personalizar las funciones que admiten composición, pero que siga siendo sencillo invocar el comportamiento predeterminado. Así, por ejemplo, puedes crear un elemento de texto simple como este:

Text(text = "Hello, Android!")

El código tiene el mismo efecto que el código mucho más detallado que se incluye a continuación, en el que gran parte de los parámetros Text se establecen de manera explícita:

Text(text = "Hello, Android!",
    color = Color.Unset,
    fontSize = TextUnit.Inherit,
    letterSpacing = TextUnit.Inherit,
    Overflow = TextOverflow.Clip)

El primer fragmento de código no solo es más simple y fácil de leer, sino que también se autodocumenta. Si especificas solo el parámetro text, documentas que para todos los demás parámetros deseas usar los valores predeterminados. Por el contrario, el segundo fragmento implica que quieres establecer valores explícitos para esos otros parámetros, aunque los valores que establezcas resulten ser los predeterminados para la función.

Funciones de orden superior y expresiones lambda

Kotlin admite funciones de orden superior, que reciben otras funciones como parámetros. Compose se basa en este enfoque. Por ejemplo, la función que admite composición Button proporciona un parámetro lambda onClick. El valor de ese parámetro es una función, a la que llama el botón cuando el usuario hace clic en él:

Button( // …
    onClick = myClickFunction)

Las funciones de orden superior se vinculan naturalmente con expresiones lambda, que evalúan a una función. Si solo necesitas usar la función una vez, no tienes que definirla en otra parte para pasarla a la función de orden superior. En su lugar, puedes definirla directamente con una expresión lambda. En el ejemplo anterior, se supone que el objeto myClickFunction() está definido en otro lugar. Sin embargo, si solo usas esa función aquí, es más fácil definirla intercalada con una expresión lambda:

Button( // ...
    onClick = {
       // do something
       // do something else
    }
)

Expresiones lambda finales

Kotlin ofrece una sintaxis especial para llamar a funciones de orden superior cuyo último parámetro es un valor lambda. Si deseas pasar una expresión lambda como ese parámetro, puedes usar la sintaxis de expresión lambda final. En lugar de colocar la expresión lambda dentro de los paréntesis, la colocas después. Esta situación es común en Compose, por lo que debes familiarizarte con la apariencia del código.

Por ejemplo, el último parámetro de todos los diseños, como la función que admite composición Column(), es children, una función que emite los elementos secundarios de la IU. Supongamos que deseas crear una columna que contenga tres elementos de texto y necesitas aplicar formato. Este código funciona, pero es muy engorroso:

Column(
    modifier = Modifier.padding(16.dp), children = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

Como el parámetro children es el último de la firma de la función y pasamos su valor como expresión lambda, podemos extraerlo de los paréntesis:

Column(modifier = Modifier.padding(16.dp)) {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

Los dos ejemplos tienen exactamente el mismo significado. Las llaves definen la expresión lambda que se pasa al parámetro content.

De hecho, si el único parámetro que pasas es esa expresión lambda final (es decir, si el parámetro final es un valor lambda y no pasas ningún otro parámetro), puedes omitir los paréntesis por completo. Por ejemplo, supongamos que no necesitaste pasar ningún modificador a Column, el código podrías escribirlo así:

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

Esta sintaxis es bastante común en Compose, en especial para elementos de diseño como Column. El último parámetro es una expresión lambda que define los elementos secundarios, y esos elementos secundarios se especifican en llaves después de la llamada a función.

Alcances y receptores

Algunos métodos y propiedades solo están disponibles dentro de determinado alcance. El alcance limitado permite ofrecer la funcionalidad cuando sea necesario y evitar que se use por error cuando no corresponda.

Aquí tienes un ejemplo que se usa en Compose. Cuando llamas al elemento que admite composición del diseño Row, tu lambda de contenido se invoca automáticamente dentro de una RowScope. Eso permite que el objeto Row exponga funcionalidades que solo son válidas dentro de un objeto Row. En el siguiente ejemplo, se muestra cómo el objeto Row expuso un valor específico de fila para el modificador gravity:

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.gravity(Alignment.CenterVertically)
    )
}

Algunas API aceptan lambdas que se llaman dentro del alcance del receptor. Esas lambdas tienen acceso a propiedades y funciones que se definen en otro lugar, según la declaración de parámetro:

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.
        drawRectangle(...)
    }
)

Para obtener más información, consulta los literales de función con receptores en la documentación de Kotlin.

Propiedades delegadas

Kotlin admite propiedades delegadas, que se llaman como si fueran campos, pero su valor se determina de forma dinámica mediante la evaluación de una expresión. Puedes reconocer esas propiedades por el uso de la sintaxis by:

class delegatingClass {
    var name: String by nameGetterFunction()
}

Otro código puede acceder a la propiedad con un código como este:

myDC = delegatingClass()
println("The name property is: " + myDC.name)

Cuando se ejecuta println(), se llama a nameGetterFunction() para mostrar el valor de la string.

Esas propiedades delegadas son particularmente útiles cuando trabajas con propiedades respaldadas por estados:

var showDialog by remember { mutableStateOf(false) }

// Updating the var automatically triggers a state change
showDialog = true

Desestructuración de clases de datos

Si defines una clase de datos, puedes acceder con facilidad a ellos mediante una declaración de desestructuración. Por ejemplo, supongamos que defines una clase Person:

data class Person(val name: String, val age: Int)

Si tienes un objeto de ese tipo, puedes acceder a sus valores con un código como este:

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

A menudo, verás ese tipo de código en las funciones de Compose:

ConstraintLayout {

    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.

    // ...

}

Las clases de datos proporcionan muchas otras funcionalidades útiles. Por ejemplo, cuando defines una clase de datos, el compilador define automáticamente funciones útiles como equals() y copy(). Puedes obtener más información en la documentación sobre clases de datos.

Objetos singleton

Kotlin facilita la declaración de singletons, clases que siempre tienen una sola instancia. Esos singleton se declaran con la palabra clave object. Compose a menudo los usa. Por ejemplo, el objeto MaterialTheme se define como un objeto singleton. Las propiedades MaterialTheme.colors, shapes y typography contienen los valores para el tema actual.