Aspectos básicos de Jetpack Compose

1. Antes de comenzar

Jetpack Compose es un kit de herramientas moderno diseñado para simplificar el desarrollo de IU. Combina un modelo de programación reactivo con la concisión y facilidad de uso del lenguaje de programación Kotlin. Es totalmente declarativo, lo que significa que describes tu IU si llamas a una serie de funciones que transforman datos en una jerarquía de IU. Cuando los datos subyacentes cambian, el framework vuelve a ejecutar estas funciones automáticamente y actualiza la jerarquía de la IU por ti.

Una app de Compose tiene funciones de componibilidad, que son solo funciones normales marcadas con @Composable, que pueden llamar a otras funciones de componibilidad. Una función es todo lo que necesitas para crear un nuevo componente de IU. La anotación le indica a Compose que agregue compatibilidad especial a la función para actualizar y mantener tu IU con el tiempo. Compose te permite estructurar el código en fragmentos pequeños. A menudo, las funciones de componibilidad también se denominan "elementos componibles".

Cuando haces pequeños elementos componibles reutilizables, es fácil crear una biblioteca de elementos de IU que se usan en tu app. Cada uno es responsable de una parte de la pantalla y se puede editar de forma independiente.

Para obtener más asistencia mientras realizas este codelab, consulta el siguiente código:

Nota: El código usa Material 2, mientras que el codelab se actualizó para usar Material 3. Ten en cuenta que algunos pasos serán diferentes.

Requisitos previos

  • Tener experiencia con la sintaxis de Kotlin, incluidas las funciones de lambdas

Actividades

En este codelab, aprenderás lo siguiente:

  • Qué es Compose
  • Cómo compilar IUs con Compose
  • Cómo administrar el estado en funciones de componibilidad
  • Cómo crear una lista de rendimiento
  • Cómo agregar animaciones
  • Cómo aplicarle estilos y temas a una app

Compilarás una app con una pantalla de integración y una lista de elementos animados expandidos:

8d24a786bfe1a8f2.gif

Requisitos

2. Cómo comenzar un nuevo proyecto en Compose

Para iniciar un nuevo proyecto de Compose, abre Android Studio.

Si te encuentras en la ventana Welcome to Android Studio, haz clic en Start a new Android Studio project. Si ya tienes abierto un proyecto de Android Studio, selecciona File > New > New Project en la barra de menú.

Para crear un proyecto nuevo, selecciona Empty Activity en las plantillas disponibles.

d12472c6323de500.png

Haz clic en Next y configura tu proyecto como de costumbre; llámalo "BasicsCodelab". Asegúrate de seleccionar una minimumSdkVersion del nivel de API 21 como mínimo, que es la mínima que admite la API de Compose.

Cuando elijas la plantilla Empty Activity, se generará el siguiente código en tu proyecto:

  • El proyecto ya está configurado para usar Compose.
  • Se creó el archivo AndroidManifest.xml.
  • Los archivos build.gradle.kts y app/build.gradle.kts contienen opciones y dependencias necesarias para Compose.

Después de sincronizar el proyecto, abre MainActivity.kt y revisa el código.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting("Android")
    }
}

En la siguiente sección, verás lo que hace cada método y cómo puedes mejorarlos para crear diseños flexibles y reutilizables.

Solución del codelab

Puedes obtener el código de la solución de este codelab en GitHub:

$ git clone https://github.com/android/codelab-android-compose

También tienes la opción de descargar el repositorio como archivo ZIP:

Encontrarás el código de la solución en el proyecto BasicsCodelab. Te recomendamos que sigas este codelab paso a paso, a tu ritmo y que compruebes la solución si es necesario. Durante el codelab, recibirás fragmentos de código que deberás agregar al proyecto.

3. Cómo comenzar a usar Compose

Revisa las diferentes clases y métodos relacionados con Compose que Android Studio generó por ti.

Funciones de componibilidad

Una función de componibilidad es una función normal con una anotación @Composable. Esto permite que tu función llame a otras funciones de @Composable dentro de ella. Puedes ver cómo la función Greeting está marcada como @Composable. Esta función producirá una parte de la jerarquía de la IU que mostrará la entrada especificada, String. Text es una función de componibilidad y que proporciona la biblioteca.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

Compose en una app para Android

Con Compose, una Activity sigue siendo el punto de entrada a una app para Android. En nuestro proyecto, se inicia MainActivity cuando el usuario abre la app (como se especifica en el archivo AndroidManifest.xml). Usarás setContent para definir tu diseño, pero, en lugar de usar un archivo en formato XML como lo harías en el sistema tradicional de View, llamarás a funciones de componibilidad dentro de él.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                  modifier = Modifier.fillMaxSize(),
                  color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme es una forma de diseñar funciones de componibilidad. Encontrarás más información al respecto en la sección Temas de tu app. Para ver cómo se muestra el texto en la pantalla, puedes ejecutar la app en un emulador o dispositivo, o usar la vista previa de Android Studio.

Para usar la vista previa de Android Studio, solo debes marcar cualquier función de componibilidad sin parámetros o función con parámetros predeterminados con la anotación @Preview y compilar tu proyecto. Ya puedes ver una función Preview Composable en el archivo MainActivity.kt. Puedes tener varias vistas previas en el mismo archivo y asignarles un nombre.

@Preview(showBackground = true, name = "Text preview")
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

fb011e374b98ccff.png

Es posible que la vista previa no aparezca si se selecciona Code eeacd000622ba9b.png. Haz clic en Split 7093def1e32785b2.png para obtener la vista previa.

4. Cómo ajustar la IU

Primero, configura un color de fondo diferente para Greeting. Para ello, une el elemento Text componible con una Surface. Surface toma un color, por lo que debes usar MaterialTheme.colorScheme.primary.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

Los componentes anidados dentro de Surface se dibujarán sobre ese color de fondo.

Puedes ver los cambios nuevos en la vista previa:

c88121ec49bde8c7.png

Es posible que hayas pasado por alto un detalle importante: el texto ahora es blanco. ¿Cuándo definimos esto?

¡No lo hicimos! Los componentes de Material, como androidx.compose.material3.Surface, se diseñaron para mejorar tu experiencia mediante el cuidado de las funciones comunes que quizás quieras usar en tu app, como elegir un color adecuado para el texto. Decimos que Material es dogmático porque proporciona buenas opciones predeterminadas y patrones que son comunes en la mayoría de las apps. Los componentes de Material en Compose están compilados sobre otros componentes fundamentales (en androidx.compose.foundation), a los que también se puede acceder desde los componentes de tu app en caso de que necesites mayor flexibilidad.

En este caso, Surface entiende que, cuando el fondo se establece en el color primary, cualquier texto sobre él debe usar el color onPrimary, que también se define en el tema. Para obtener más información, consulta la sección Temas de tu app.

Modificadores

La mayoría de los elementos de la IU de Compose, como Surface y Text, aceptan un parámetro modifier opcional. Los modificadores le indican a un elemento de la IU cómo aparecer o comportarse en su diseño de nivel superior. Es posible que ya hayas notado que el elemento componible Greeting ya tiene un modificador predeterminado, que luego se pasa al Text.

Por ejemplo, el modificador padding aplicará una cantidad de espacio alrededor del elemento que decora. Puedes crear un modificador de padding con Modifier.padding(). También puedes agregar varios modificadores si los encadenas, por lo que, en este caso, podemos agregar el modificador de padding al predeterminado: modifier.padding(24.dp).

Ahora, agrega padding a tu Text en la pantalla:

import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

ef14f7c54ae7edf.png

Hay decenas de modificadores que se pueden usar para alinear, animar, diseñar, hacer que se pueda hacer clic o desplazarse sobre un elemento, transformar, etc. Si lo deseas, puedes consultar la Lista de modificadores de Compose. Usarás algunos en los pasos siguientes.

5. Cómo reutilizar elementos componibles

Cuantos más componentes agregues a la IU, más niveles de anidación crearás. Esto puede afectar la legibilidad si una función se vuelve muy grande. Cuando haces pequeños componentes reutilizables, es fácil crear una biblioteca de elementos de IU que se usan en tu app. Cada uno es responsable de una parte de la pantalla y se puede editar de forma independiente.

Como práctica recomendada, tu función debe incluir un parámetro modificador que tenga asignado un modificador vacío de forma predeterminada. Reenvía este modificador al primer elemento componible que llames dentro de tu función. De esta manera, el sitio que realiza la llamada puede adaptar las instrucciones de diseño y los comportamientos desde fuera de la función de componibilidad.

Crea un elemento de componibilidad llamado MyApp que incluya el saludo.

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

Eso te permite limpiar la devolución de llamada onCreate y la vista previa, ya que ahora puedes volver a usar el elemento componible MyApp y evitar la duplicación de código.

En la vista previa, llamaremos a MyApp y quitaremos el nombre de la vista previa.

Tu archivo MainActivity.kt debería tener el siguiente aspecto:

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. Cómo crear columnas y filas

Los tres elementos de diseño estándar básicos de Compose son Column, Row y Box.

518dbfad23ee1b05.png

Son funciones de componibilidad y que toman contenido de este tipo, por lo que puedes colocar elementos dentro de ellas. Por ejemplo, cada elemento secundario dentro de un Column se posicionará verticalmente.

// Don't copy over
Column {
    Text("First row")
    Text("Second row")
}

Ahora intenta cambiar Greeting para que muestre una columna con dos elementos de texto, como en este ejemplo:

bf27ee688c3231df.png

Ten en cuenta que es posible que debas mover el padding.

Compare su resultado con esta solución:

import androidx.compose.foundation.layout.Column
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Column(modifier = modifier.padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Compose y Kotlin

Las funciones de componibilidad se pueden usar como cualquier otra función en Kotlin. Esto hace que la creación de IUs sea muy eficaz, ya que puedes agregar sentencias para influir en la forma en que se mostrará la IU.

Por ejemplo, puedes usar un bucle for para agregar elementos a la Column:

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

a7ba2a8cb7a7d79d.png

Todavía no configuraste dimensiones ni agregaste restricciones al tamaño de tus elementos componibles, por lo que cada fila ocupará el espacio mínimo posible y la vista previa hará lo mismo. Cambiaremos nuestra vista previa para emular un ancho común de un teléfono pequeño de 320dp. Agrega un parámetro widthDp a la anotación @Preview de la siguiente manera:

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

a5d5f6cdbdd918a2.png

Los modificadores se usan ampliamente en Compose, así que practiquemos con un ejercicio más avanzado: intenta replicar el siguiente diseño con los modificadores fillMaxWidth y padding.

a9599061cf49a214.png

Ahora, compara tu código con la solución:

import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Ten en cuenta lo siguiente:

  • Los modificadores pueden tener sobrecargas, por ejemplo, puedes especificar diferentes formas de crear un padding.
  • Para agregar varios modificadores a un elemento, solo debes encadenarlos.

Hay varias formas de lograr este resultado. Por lo tanto, si tu código no coincide con este fragmento, no significa que sea incorrecto. Sin embargo, copia y pega este código para continuar con el codelab.

Cómo agregar un botón

En el siguiente paso, agregarás un elemento en el que se pueda hacer clic que expande el Greeting, por lo que primero debemos agregar ese botón. El objetivo es crear el siguiente diseño:

ff2d8c3c1349a891.png

Button es un elemento componible que proporciona el paquete de material3 que toma un elemento componible como último argumento. Dado que las lambdas finales se pueden mover fuera de los paréntesis, puedes agregar cualquier contenido al botón como un elemento secundario. Por ejemplo, Text:

// Don't copy yet
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}

Para lograr esto, debes aprender a colocar un elemento componible al final de una fila. Como no hay un modificador alignEnd, debes proporcionar weight al elemento componible al comienzo. El modificador weight hace que el elemento rellene todo el espacio disponible, lo que lo hace flexible y quita de manera eficaz los demás elementos que no tienen un peso, que se denominan inflexibles. También hace que el modificador fillMaxWidth sea redundante.

Ahora intenta agregar el botón y colocarlo como se muestra en la imagen anterior.

Consulta la solución aquí:

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

7. Estado en Compose

En esta sección, agregarás interacción a la pantalla. Hasta ahora, creaste diseños estáticos, pero ahora los harás reaccionar a los cambios del usuario para lograrlo:

6675d41779cac69.gif

Antes de aprender cómo hacer un botón en el que se puede hacer clic y cómo cambiar el tamaño de un elemento, debes almacenar en algún lugar un valor que indique si cada elemento está expandido o no: el estado del elemento. Como necesitamos tener uno de estos valores por saludo, el lugar lógico para este es en el elemento Greeting de componibilidad. Observa este booleano expanded y cómo se usa en el código:

// Don't copy over
@Composable
fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

Ten en cuenta que también agregamos una acción onClick y un texto de botón dinámico. Hablaremos sobre eso más adelante.

Sin embargo, esto no funcionará según lo esperado. Configurar un valor diferente para la variable expanded no hará que Compose lo detecte como un cambio de estado, por lo que no sucederá nada.

El motivo por el que la mutación de esta variable no activa recomposiciones es que Compose no está realizando un seguimiento. Además, cada vez que se llama a Greeting, la variable se restablece como falsa.

Para agregar un estado interno a un elemento componible, puedes usar la función mutableStateOf, que hace que Compose recomponga funciones que lean ese State.

import androidx.compose.runtime.mutableStateOf
// ...

// Don't copy over
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // Don't do this!
}

Sin embargo, no puedes asignar mutableStateOf a una variable dentro de un elemento componible. Como se explicó anteriormente, la recomposición puede ocurrir en cualquier momento que se vuelva a llamar al elemento componible, lo que restablecerá el estado a un nuevo estado mutable con un valor de false.

Para preservar el estado entre recomposiciones, recuerda el estado mutable con remember.

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...

@Composable
fun Greeting(...) {
    val expanded = remember { mutableStateOf(false) }
    // ...
}

remember se usa para protegerse contra la recomposición, por lo que no se restablece el estado.

Ten en cuenta que si llamas al mismo elemento componible desde diferentes partes de la pantalla, crearás diferentes elementos de IU, cada uno con su propia versión del estado. Puedes considerar el estado interno como una variable privada en una clase.

La función de componibilidad se "suscribirá" automáticamente al estado. Si el estado cambia, los elementos de componibilidad que lean estos campos se recompondrán para mostrar las actualizaciones.

Mutación del estado y reacción ante cambios de estado

Para cambiar el estado, quizás hayas notado que Button tiene un parámetro llamado onClick, pero no toma un valor, sino que toma una función.

Puedes definir la acción que se realizará con un clic si le asignas una expresión lambda. Por ejemplo, activa o desactiva el valor del estado expandido y muestra un texto diferente según el valor.

ElevatedButton(
    onClick = { expanded.value = !expanded.value },
) {
   Text(if (expanded.value) "Show less" else "Show more")
}

Ejecuta la app en modo interactivo para ver el comportamiento.

374998ad358bf8d6.png

Cuando se hace clic en el botón, se activa o desactiva expanded, lo que activa una recomposición del texto dentro del botón. Cada Greeting mantiene su propio estado expandido, ya que pertenecen a diferentes elementos de la IU.

93d839b53b7d9bea.gif

El código hasta este punto:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Cómo expandir el elemento

Ahora expandamos un elemento cuando se solicita. Agrega una variable adicional que dependa de nuestro estado:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
// ...

No necesitas recordar extraPadding según la recomposición, ya que realiza un cálculo simple.

Ahora, podemos aplicar un nuevo modificador de padding a la columna:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Si ejecutas un emulador o en el modo interactivo, deberías ver que cada elemento se puede expandir de forma independiente:

6675d41779cac69.gif

8. Elevación de estado

En las funciones de componibilidad, el estado que leen o modifican varias funciones debe encontrarse en un principal común. Este proceso se conoce como elevación de estado. Elevar en el sentido de subir.

Hacer que el estado sea elevable evita la duplicación del estado y la generación de errores, ayuda a reutilizar elementos componibles y facilita mucho las pruebas con ellos. Por otro lado, el estado que no necesita ser controlado por el elemento superior de componibilidad no debe elevarse. La fuente de confianza pertenece al usuario que crea y controla ese estado.

Por ejemplo, creemos una pantalla de integración para nuestra app.

5d5f44508fcfa779.png

Agrega el siguiente código a MainActivity.kt:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = { shouldShowOnboarding = false }
        ) {
            Text("Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen()
    }
}

Este código incluye varias funciones nuevas:

  • Agregaste un nuevo elemento de componibilidad llamado OnboardingScreen y también una nueva vista previa. Si compilas el proyecto, notarás que puedes tener varias vistas previas al mismo tiempo. También agregamos una altura fija para verificar que el contenido se alinee correctamente.
  • Se puede configurar Column para que muestre su contenido en el centro de la pantalla.
  • shouldShowOnboarding usa una palabra clave by en lugar de =. Este es un delegado de propiedad para que no debas escribir .value cada vez.
  • Cuando se hace clic en el botón, shouldShowOnboarding se configura en false; sin embargo, aún no se lee el estado desde ningún lugar.

Ahora podemos agregar esta nueva pantalla de integración a nuestra app. Queremos que se muestre en el inicio, luego, ocultarla cuando el usuario presione "Continuar".

En Compose no ocultas elementos de la IU. En su lugar, no los agregas a la composición, de modo que no se agregan al árbol de IU que genera Compose. Esto se hace con la lógica condicional simple de Kotlin. Por ejemplo, para mostrar la pantalla de integración o la lista de saludos, harías algo como lo siguiente:

// Don't copy yet
@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(modifier) {
        if (shouldShowOnboarding) { // Where does this come from?
            OnboardingScreen()
        } else {
            Greetings()
        }
    }
}

Sin embargo, no tenemos acceso a shouldShowOnboarding. Está claro que necesitamos compartir el estado que creamos en OnboardingScreen con el elemento componible MyApp.

En lugar de compartir el valor del estado con su elemento superior, elevamos el estado; lo cambiamos al principal común que necesita acceder a él.

Primero, mueve el contenido de MyApp a un nuevo elemento componible llamado Greetings. También adapta la vista previa para llamar al método Greetings en su lugar:

@Composable
fun MyApp(modifier: Modifier = Modifier) {
     Greetings()
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Agrega una vista previa de nuestro nuevo elemento componible MyApp de nivel superior de modo que podamos probar su comportamiento:

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

Ahora, agrega la lógica para mostrar las diferentes pantallas en MyApp y eleva el estado.

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(/* TODO */)
        } else {
            Greetings()
        }
    }
}

También debemos compartir shouldShowOnboarding con la pantalla de integración, pero la pasaremos directamente. En lugar de permitir que OnboardingScreen modifique nuestro estado, sería mejor que nos notifique cuando el usuario haga clic en el botón Continuar.

¿Cómo pasamos eventos? Si pasamos las devoluciones de llamada a otros elementos. Las devoluciones de llamada son funciones que se pasan como argumentos a otras funciones y se ejecutan cuando se produce el evento.

Intenta agregar un parámetro de función a la pantalla de integración definida como onContinueClicked: () -> Unit para que puedas mutar el estado de MyApp.

Solución:

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier
                .padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

Cuando pasas una función y no un estado a OnboardingScreen, hacemos que este elemento componible sea más reutilizable y evitamos que otros elementos componibles muten el estado. En general, simplifica la tarea. Un buen ejemplo es cómo se debe modificar la vista previa de integración para llamar a OnboardingScreen ahora:

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
    }
}

Asignar onContinueClicked a una expresión lambda vacía significa "no hacer nada", lo que es perfecto para una vista previa.

Esto se parece cada vez más a una app real. ¡Buen trabajo!

25915eb273a7ef49.gif

En el elemento componible MyApp, usamos el delegado de propiedad by por primera vez para evitar usar el valor cada vez. Usemos by en lugar de = también en el elemento componible Greeting para la propiedad expanded. Asegúrate de cambiar expanded de val a var.

El código completo hasta ahora:

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding = if (expanded) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

9. Crea una lista diferida de rendimiento

Ahora, hagamos que la lista de nombres sea más realista. Hasta ahora, mostraste dos saludos en un Column. Pero ¿puedes administrar miles de ellos?

Cambia el valor de lista predeterminado en los parámetros de Greetings para usar otro constructor de lista que permita establecer el tamaño de la lista y completarla con el valor contenido en su lambda (aquí $it representa el índice de la lista):

names: List<String> = List(1000) { "$it" }

De esta manera, se crean 1,000 saludos, incluso los que no caben en la pantalla. Obviamente, esto no es un buen rendimiento. Puedes intentar ejecutarlo en un emulador (ten en cuenta que este código puede congelar el emulador).

Para mostrar una columna desplazable, usamos LazyColumn. LazyColumn procesa solo los elementos visibles en la pantalla, lo que permite obtener mejoras de rendimiento cuando se renderiza una lista grande.

En su uso básico, la API de LazyColumn proporciona un elemento items dentro de su alcance, en el que se escribe la lógica de renderización individual:

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
// ...

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

284f925eb984fb56.gif

10. Estado persistente

Nuestra app tiene dos problemas:

Persiste el estado de la pantalla de integración

Si ejecutas la app en un dispositivo, haces clic en los botones y luego lo rotas, se volverá a mostrar la pantalla de integración. La función remember se ejecuta siempre y cuando el elemento componible se mantenga en la composición. Cuando giras, se reinicia toda la actividad y se pierde todo el estado. Esto también sucede con cualquier cambio de configuración o cuando finaliza el proceso.

En lugar de usar remember, puedes usar rememberSaveable. Esto guardará todos los estados que sobrevivan a los cambios de configuración (como las rotaciones) y el cierre del proceso.

Ahora, reemplaza el uso de remember en shouldShowOnboarding con rememberSaveable:

    import androidx.compose.runtime.saveable.rememberSaveable
    // ...

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

Ejecuta, rota, cambia al modo oscuro o finaliza el proceso. No se muestra la pantalla de integración, a menos que hayas salido de la app anteriormente.

Persiste el estado expandido de los elementos de lista

Si expandes un elemento de la lista y luego te desplazas por la lista hasta que el elemento quede fuera de la vista, o rotas el dispositivo y luego regresas al elemento expandido, verás que el elemento ahora vuelve a su estado inicial.

La solución es usar rememberSaveable para el estado expandido también:

   var expanded by rememberSaveable { mutableStateOf(false) }

Con aproximadamente 120 líneas de código hasta ahora, pudiste mostrar una lista de desplazamiento larga y con buen rendimiento de los elementos, cada uno con su propio estado. Además, como puedes ver, tu app tiene un modo oscuro correcto sin líneas de código adicionales. Aprenderás sobre los temas más adelante.

11. Cómo animar tu lista

En Compose, existen varias maneras de animar tu IU: desde APIs de alto nivel para animaciones simples hasta métodos de bajo nivel para un control total y transiciones complejas. Obtén más información al respecto en la documentación.

En esta sección, usarás una de las APIs de bajo nivel, pero no te preocupes; también pueden ser muy simples. Animemos el cambio de tamaño que ya implementamos:

9efa14ce118d3835.gif

Para ello, usarás el elemento componible animateDpAsState. Muestra un objeto State cuyo value se actualizará de forma continua en la animación hasta que finalice. Toma un "valor objetivo" cuyo tipo es Dp.

Crea un extraPadding animado que dependa del estado expandido.

import androidx.compose.animation.core.animateDpAsState

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

Ejecuta la app y prueba la animación.

animateDpAsState toma un parámetro opcional animationSpec que te permite personalizar la animación. Hagamos algo más divertido, como agregar una animación basada en resortes:

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
    // ...
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))

    // ...

    )
}

Ten en cuenta que también nos aseguramos de que el padding nunca sea negativo; de lo contrario, podría fallar la app. Esto genera un error de animación sutil que corregiremos más adelante en Retoques finales.

La especificación de spring no toma ningún parámetro relacionado con el tiempo, sino que se basa en propiedades físicas (amortiguación y rigidez) para que las animaciones sean más naturales. Ejecuta la app ahora para probar la nueva animación:

9efa14ce118d3835.gif

Cualquier animación creada con animate*AsState puede ser interrumpida. Esto significa que si el valor objetivo cambia en medio de la animación, animate*AsState la reinicia y apunta al valor nuevo. Las interrupciones se ven más naturales en especial con las animaciones basadas en resortes:

d5dbf92de69db775.gif

Si quieres explorar los diferentes tipos de animaciones, prueba diferentes parámetros para spring, diferentes especificaciones (tween, repeatable) y diferentes funciones: animateColorAsState o un tipo distinto de API de Animation.

Código completo de esta sección

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

12. Cómo aplicar estilo y temas a tu app

Hasta ahora, no diseñaste ninguno de los elementos componibles y, de todos modos, obtuviste una configuración predeterminada aceptable, incluida la compatibilidad con el modo oscuro. Veamos qué son BasicsCodelabTheme y MaterialTheme.

Si abres el archivo ui/theme/Theme.kt, verás que BasicsCodelabTheme usa MaterialTheme en su implementación:

// Do not copy
@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // ...

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

MaterialTheme es una función de componibilidad que refleja los principios de estilo de la especificación de Material Design. Esa información de estilo cae en cascada a los componentes que están dentro de su content, que pueden leer la información para diseñar su propio estilo. En tu IU, ya estás usando BasicsCodelabTheme de la siguiente manera:

    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }

Debido a que BasicsCodelabTheme une MaterialTheme internamente, MyApp tiene un estilo con las propiedades definidas en el tema. Desde cualquier elemento componible descendiente, puedes recuperar tres propiedades de MaterialTheme: colorScheme, typography y shapes. Úsalas para establecer un estilo de encabezado de uno de tus Text:

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }

El elemento de componibilidad Text del ejemplo anterior configura un TextStyle nuevo. Puedes crear tu propio TextStyle o recuperar un estilo definido por el tema con MaterialTheme.typography, que es el método preferido. Esta construcción te brinda acceso a los estilos de texto definidos por Material, como displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium, entre otros. En tu ejemplo, debes usar el estilo headlineMedium definido en el tema.

Ahora, compila el código para ver el texto con estilo nuevo:

673955c38b076f1c.png

En general, es mucho mejor mantener los colores, las formas y los estilos de fuente dentro de un MaterialTheme. Por ejemplo, el modo oscuro sería difícil de implementar si el código está hard-coded y requeriría mucho trabajo propenso a errores solucionarlo.

Sin embargo, a veces es necesario desviarse un poco de la selección de colores y estilos de fuente. En esas situaciones, es mejor basar el color o el estilo en uno existente.

Para ello, puedes modificar un estilo predefinido con la función copy. Haz que el número quede en extranegrita:

import androidx.compose.ui.text.font.FontWeight
// ...
Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

De esta manera, si necesitas cambiar la familia de fuentes o cualquier otro atributo de headlineMedium, no tendrás que preocuparte por las pequeñas desviaciones.

Este debería ser el resultado en la ventana de vista previa:

b33493882bda9419.png

Cómo configurar una vista previa del modo oscuro

Actualmente, nuestra vista previa solo muestra cómo se verá la app en el modo claro. Agrega una anotación @Preview adicional a GreetingPreview con UI_MODE_NIGHT_YES:

import android.content.res.Configuration.UI_MODE_NIGHT_YES

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Esto agregará una vista previa en modo oscuro.

2c94dc7775d80166.png

Cómo ajustar el tema de tu app

Puedes encontrar todo lo relacionado con el tema actual en los archivos dentro de la carpeta ui/theme. Por ejemplo, los colores predeterminados que usamos hasta ahora se definen en Color.kt.

Comencemos por definir colores nuevos. Agrégalos a Color.kt:

val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)

Ahora asígnalos a la paleta de MaterialTheme en Theme.kt:

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

Si vuelves a MainActivity.kt y actualizas la vista previa, los colores de la vista previa no se modificarán. Esto se debe a que, de forma predeterminada, la vista previa usará colores dinámicos. Puedes ver la lógica para agregar colores dinámicos en Theme.kt con el parámetro booleano dynamicColor.

Si deseas ver la versión no adaptable de tu esquema de colores, ejecuta tu app en un dispositivo con nivel de API anterior al 31 (correspondiente a Android S, en el que se introdujeron los colores adaptables). Verás los nuevos colores:

493d754584574e91.png

En Theme.kt, define la paleta para colores oscuros:

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Cuando ejecutemos la app, veremos los colores oscuros en acción:

84d2a903ffa6d8df.png

Código final para Theme.kt

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

13. Toques finales

En este paso, aplicarás lo que ya conoces y aprenderás nuevos conceptos con solo unas pocas sugerencias. Crearás lo siguiente:

8d24a786bfe1a8f2.gif

Cómo reemplazar un botón con un ícono

  • Usa el elemento IconButton componible junto con un elemento secundario Icon.
  • Usa Icons.Filled.ExpandLess y Icons.Filled.ExpandMore, que están disponibles en el artefacto material-icons-extended. Agrega la siguiente línea a las dependencias en el archivo app/build.gradle.kts.
implementation("androidx.compose.material:material-icons-extended")
  • Modifica el padding para corregir la alineación.
  • Agrega una descripción de contenido para accesibilidad (consulta "Cómo usar recursos de strings" a continuación).

Cómo usar recursos de strings

La descripción del contenido de "Mostrar más" y "Mostrar menos" debe estar presente, y puedes agregarla con una sentencia if simple:

contentDescription = if (expanded) "Show less" else "Show more"

Sin embargo, las strings hard-coded son una práctica no recomendada y deberías obtenerlas del archivo strings.xml.

Puedes usar "Extract string resource" en cada cadena, disponible en "Context Actions" en Android Studio, para hacerlo automáticamente.

También puedes abrir app/src/res/values/strings.xml y agregar los siguientes recursos:

<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

Mostrar más

El texto "Composem ipsum" aparece y desaparece, lo que activa un cambio en el tamaño de cada tarjeta.

  • Agrega un nuevo Text a la columna dentro de Greeting que se muestre cuando se expanda el elemento.
  • Quita el extraPadding y, en su lugar, aplica el modificador animateContentSize a la Row. Esto automatizará el proceso de creación de una animación, algo que sería difícil de hacer de forma manual. Además, quita la necesidad de coerceAtLeast.

Cómo agregar elevación y formas

  • Puedes usar el modificador shadow junto con el modificador clip para lograr la apariencia de la tarjeta. Sin embargo, hay un elemento componible de Material que hace exactamente eso: Card. Para cambiar los colores de Card, llama a CardDefaults.cardColors y anula el color que quieras cambiar.

Código final

package com.example.basicscodelab

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier, color = MaterialTheme.colorScheme.background) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by rememberSaveable { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name, style = MaterialTheme.typography.headlineMedium.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }
            )
        }
    }
}

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

14. Felicitaciones

¡Felicitaciones! Ya aprendiste los conceptos básicos de Compose.

Solución del codelab

Puedes obtener el código de la solución de este codelab en GitHub:

$ git clone https://github.com/android/codelab-android-compose

También tienes la opción de descargar el repositorio como archivo ZIP:

¿Qué sigue?

Consulta los otros codelabs sobre la ruta de aprendizaje de Compose:

Lecturas adicionales