Tema 1B. Introducción a Jetpack Compose

  • Bloque: B1 — Fundamentos: Kotlin, Compose y entorno Android
  • Duración aproximada: 6 horas
  • RA1 — Aplica tecnologías de desarrollo para dispositivos móviles evaluando sus características y capacidades.
Código Criterio
RA1-b Se han identificado las tecnologías de desarrollo de aplicaciones para dispositivos móviles.
RA1-c Se han instalado, configurado y utilizado entornos de trabajo para el desarrollo de aplicaciones para dispositivos móviles.
RA1-f Se ha analizado la estructura de aplicaciones existentes para dispositivos móviles identificando las clases utilizadas.
RA1-g Se han realizado modificaciones sobre aplicaciones existentes.
RA1-h Se han utilizado emuladores para comprobar el funcionamiento de las aplicaciones.

1. ¿Qué es Jetpack Compose?#

Jetpack Compose es el framework moderno de Android para construir interfaces de usuario de forma declarativa. Fue publicado de forma estable por Google en 2021 y se ha convertido en la forma oficial y recomendada de desarrollar interfaces en Android, sustituyendo progresivamente al sistema clásico basado en archivos XML.

1.1 Declarativo vs imperativo#

El cambio de paradigma es conceptualmente importante. Merece la pena entenderlo bien antes de escribir código.

Sistema clásico (imperativo con XML): La UI se define en archivos XML y se manipula desde Kotlin/Java. El desarrollador le dice al sistema cómo cambiar la UI paso a paso.

1<!-- activity_main.xml -->
2<TextView android:id="@+id/textContador" android:text="0" />
3<Button android:id="@+id/btnIncrementar" android:text="+" />
1// MainActivity.kt — manipulación imperativa
2var contador = 0
3btnIncrementar.setOnClickListener {
4    contador++
5    textContador.text = contador.toString()   // actualiza manualmente la vista
6}

Compose (declarativo): La UI se describe en función del estado. Cuando el estado cambia, Compose recalcula y actualiza automáticamente la parte afectada de la UI. El desarrollador dice qué mostrar, no cómo actualizarlo.

 1// Compose — la UI es una función del estado
 2@Composable
 3fun ContadorScreen() {
 4    var contador by remember { mutableIntStateOf(0) }
 5
 6    Column {
 7        Text(text = "$contador")   // siempre muestra el valor actual de contador
 8        Button(onClick = { contador++ }) {
 9            Text("+")
10        }
11    }
12    // No hay que decirle al Text que se actualice: Compose lo hace automáticamente
13}

1.2 Ventajas principales#

  • Menos código: no se necesitan archivos XML, findViewById, ni adaptadores (Adapters).
  • Reactividad automática: la UI se actualiza sola cuando cambia el estado.
  • Kotlin nativo: aprovecha todas las características del lenguaje (lambdas, extensiones, etc.).
  • Previsualizaciones: permite ver la UI en Android Studio sin ejecutar la app.
  • Interoperabilidad: se puede combinar con vistas XML antiguas cuando sea necesario.

2. Arquitectura interna de Compose#

Compose está formado por tres capas que trabajan de forma coordinada pero independiente:

┌─────────────────────────────────────────────────────────────┐
│  Compose UI                                                 │
│  Elementos visuales: Text, Button, Column, Row, Image...    │
│  Es donde trabaja el desarrollador                          │
├─────────────────────────────────────────────────────────────┤
│  Compose Runtime                                            │
│  Motor de ejecución: gestiona el estado, detecta cambios    │
│  y decide qué partes de la UI deben recomponerse            │
├─────────────────────────────────────────────────────────────┤
│  Compose Compiler                                           │
│  Plugin del compilador de Kotlin que transforma las         │
│  funciones @Composable en código optimizado                 │
└─────────────────────────────────────────────────────────────┘

Esta separación en capas hace que Compose sea extensible: se puede reemplazar la capa de UI (hay variantes para Desktop, Web y TV) manteniendo el runtime y el compilador.


3. Estructura de un proyecto Compose#

Al crear un proyecto nuevo en Android Studio con la plantilla Empty Activity, la estructura básica es:

app/
├── src/main/
│   ├── java/com.ejemplo.app/
│   │   └── MainActivity.kt       ← punto de entrada
│   ├── res/
│   │   ├── values/
│   │   │   ├── themes.xml        ← tema Material
│   │   │   └── strings.xml
│   │   └── drawable/             ← recursos gráficos
│   └── AndroidManifest.xml       ← configuración de la app
└── build.gradle.kts              ← dependencias

El archivo MainActivity.kt tiene esta forma básica:

 1class MainActivity : ComponentActivity() {
 2    override fun onCreate(savedInstanceState: Bundle?) {
 3        super.onCreate(savedInstanceState)
 4        // enableEdgeToEdge() activa el diseño de pantalla completa (Android 15+)
 5        enableEdgeToEdge()
 6        setContent {
 7            // AppTheme aplica el tema Material3 definido en themes.xml
 8            AppTheme {
 9                // Composable raíz de la app — punto de entrada de la UI
10                AppFlixApp()
11            }
12        }
13    }
14}

Dependencias necesarias#

 1// build.gradle.kts (app)
 2plugins {
 3    id("com.android.application")
 4    id("org.jetbrains.kotlin.android")
 5}
 6
 7android {
 8    compileSdk = 35
 9    defaultConfig {
10        minSdk = 26       // Android 8.0 — cubre más del 95% de dispositivos activos
11        targetSdk = 35
12    }
13    buildFeatures {
14        compose = true    // activa el compilador de Compose
15    }
16    composeOptions {
17        kotlinCompilerExtensionVersion = "1.5.15"
18    }
19}
20
21dependencies {
22    val composeBom = platform("androidx.compose:compose-bom:2024.10.00")
23    implementation(composeBom)
24
25    // Módulos de Compose (versiones gestionadas por el BOM)
26    implementation("androidx.compose.ui:ui")
27    implementation("androidx.compose.ui:ui-tooling-preview")
28    implementation("androidx.compose.material3:material3")
29    implementation("androidx.activity:activity-compose:1.9.3")
30
31    // Herramientas de desarrollo (solo en debug)
32    debugImplementation("androidx.compose.ui:ui-tooling")
33    debugImplementation("androidx.compose.ui:ui-test-manifest")
34}

¿Qué es el BOM (Bill of Materials)? Es un archivo que gestiona las versiones de todas las dependencias de Compose de forma coordinada. Al declarar platform("androidx.compose:compose-bom:..."), no es necesario especificar versiones individuales para cada módulo de Compose; el BOM garantiza que todas sean compatibles entre sí.


4. Funciones @Composable#

La unidad básica de Compose es la función composable: una función de Kotlin anotada con @Composable que describe un fragmento de la interfaz de usuario.

 1// Convención: el nombre de un Composable empieza por mayúscula (como una clase)
 2@Composable
 3fun Saludo(nombre: String) {
 4    Text(text = "Hola, $nombre")
 5}
 6
 7// Composable que puede visualizarse en el editor sin ejecutar la app
 8@Preview(showBackground = true, name = "Saludo de Ana")
 9@Composable
10fun SaludoPreview() {
11    Saludo("Ana")
12}

Reglas de las funciones @Composable#

  1. Solo pueden llamarse desde otras funciones @Composable (o desde un ámbito de composición como setContent).
  2. No deben tener efectos secundarios directos: no modificar variables externas, no escribir en base de datos, no hacer llamadas de red directamente.
  3. Deben ser idempotentes: llamarlas varias veces con los mismos parámetros debe producir el mismo resultado.
  4. Pueden ejecutarse en cualquier orden y en paralelo (el runtime optimiza esto).

Composición y recomposición#

Cuando el estado del que depende un Composable cambia, Compose vuelve a ejecutar ese Composable (y solo ese, en la medida de lo posible). Este proceso se llama recomposición:

Estado inicial        Estado tras click
─────────────         ────────────────
contador = 0          contador = 1
     ↓                      ↓
Text("0")             Text("1")   ← recompuesto automáticamente

Compose es inteligente: si el parámetro de un Composable no cambia, no lo recompone. Esto hace que la UI sea eficiente incluso con muchos elementos.


5. Layouts: Column, Row, Box#

Los layouts son Composables que organizan sus hijos en el espacio. Los tres fundamentales son:

Column — apilamiento vertical#

 1@Composable
 2fun EjemploColumn() {
 3    Column(
 4        modifier = Modifier
 5            .fillMaxSize()
 6            .padding(16.dp),
 7        horizontalAlignment = Alignment.CenterHorizontally,   // centra horizontalmente
 8        verticalArrangement = Arrangement.spacedBy(8.dp)      // espacio entre hijos
 9    ) {
10        Text("Primer elemento")
11        Text("Segundo elemento")
12        Button(onClick = { }) { Text("Botón") }
13    }
14}

Row — apilamiento horizontal#

 1@Composable
 2fun EjemploRow() {
 3    Row(
 4        modifier = Modifier.fillMaxWidth().padding(8.dp),
 5        verticalAlignment = Alignment.CenterVertically,
 6        horizontalArrangement = Arrangement.SpaceBetween   // distribución entre extremos
 7    ) {
 8        Text("Izquierda")
 9        Icon(Icons.Default.Star, contentDescription = "Favorito")
10        Text("Derecha")
11    }
12}

Box — apilamiento en profundidad (Z)#

 1@Composable
 2fun EjemploBox() {
 3    Box(
 4        modifier = Modifier.size(200.dp),
 5        contentAlignment = Alignment.Center   // centra el contenido por defecto
 6    ) {
 7        Image(
 8            painter = painterResource(R.drawable.poster),
 9            contentDescription = "Poster",
10            modifier = Modifier.fillMaxSize()
11        )
12        // Este texto se superpone sobre la imagen
13        Text(
14            text = "★ 8.5",
15            color = Color.White,
16            modifier = Modifier
17                .align(Alignment.BottomEnd)   // esquina inferior derecha
18                .padding(8.dp)
19        )
20    }
21}

Composición de layouts#

Los layouts se pueden anidar libremente para crear cualquier distribución:

 1@Composable
 2fun TarjetaPelicula(titulo: String, puntuacion: Double, posterUrl: String) {
 3    Row(
 4        modifier = Modifier
 5            .fillMaxWidth()
 6            .padding(8.dp),
 7        verticalAlignment = Alignment.Top
 8    ) {
 9        // Poster a la izquierda
10        Box(modifier = Modifier.size(80.dp, 120.dp)) {
11            // Aquí iría AsyncImage de Coil (Bloque 3)
12        }
13        Spacer(modifier = Modifier.width(12.dp))
14
15        // Información a la derecha
16        Column(modifier = Modifier.weight(1f)) {
17            Text(text = titulo, style = MaterialTheme.typography.titleMedium)
18            Spacer(modifier = Modifier.height(4.dp))
19            Text(text = "★ $puntuacion", style = MaterialTheme.typography.bodyMedium)
20        }
21    }
22}

6. Componentes básicos de UI#

Text#

 1@Composable
 2fun EjemploText() {
 3    Column(modifier = Modifier.padding(16.dp)) {
 4        // Estilo Material3
 5        Text(
 6            text = "Título principal",
 7            style = MaterialTheme.typography.headlineLarge
 8        )
 9        Text(
10            text = "Subtítulo",
11            style = MaterialTheme.typography.titleMedium,
12            color = MaterialTheme.colorScheme.primary
13        )
14        Text(
15            text = "Texto con formato avanzado usando AnnotatedString",
16            style = MaterialTheme.typography.bodyMedium,
17            maxLines = 2,
18            overflow = TextOverflow.Ellipsis   // "..." si no cabe
19        )
20    }
21}

Button y variantes#

 1@Composable
 2fun EjemploBotones() {
 3    Column(
 4        modifier = Modifier.padding(16.dp),
 5        verticalArrangement = Arrangement.spacedBy(8.dp)
 6    ) {
 7        // Botón relleno — acción principal
 8        Button(onClick = { /* acción */ }) {
 9            Icon(Icons.Default.PlayArrow, contentDescription = null)
10            Spacer(modifier = Modifier.width(4.dp))
11            Text("Reproducir")
12        }
13
14        // Botón con borde — acción secundaria
15        OutlinedButton(onClick = { }) {
16            Text("Añadir a favoritos")
17        }
18
19        // Botón de texto — acción terciaria
20        TextButton(onClick = { }) {
21            Text("Ver detalles")
22        }
23
24        // FAB (Floating Action Button)
25        FloatingActionButton(onClick = { }) {
26            Icon(Icons.Default.Add, contentDescription = "Añadir película")
27        }
28    }
29}

TextField#

 1@Composable
 2fun EjemploTextField() {
 3    var texto by remember { mutableStateOf("") }
 4
 5    OutlinedTextField(
 6        value = texto,
 7        onValueChange = { texto = it },
 8        label = { Text("Buscar película") },
 9        leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
10        trailingIcon = {
11            if (texto.isNotEmpty()) {
12                IconButton(onClick = { texto = "" }) {
13                    Icon(Icons.Default.Clear, contentDescription = "Borrar")
14                }
15            }
16        },
17        singleLine = true,
18        modifier = Modifier.fillMaxWidth()
19    )
20}

Image#

 1@Composable
 2fun EjemploImagen() {
 3    // Imagen local (de la carpeta res/drawable)
 4    Image(
 5        painter = painterResource(id = R.drawable.logo),
 6        contentDescription = "Logo de AppFlix",   // obligatorio para accesibilidad
 7        modifier = Modifier
 8            .size(120.dp)
 9            .clip(CircleShape),                    // recorte circular
10        contentScale = ContentScale.Crop           // rellena recortando si hace falta
11    )
12}

7. Modifier 🔑#

Modifier es el mecanismo de Compose para ajustar el aspecto, comportamiento y disposición de cualquier Composable. Se aplica de forma encadenada y el orden importa:

 1@Composable
 2fun EjemploModifier() {
 3    // El orden de los modificadores afecta al resultado visual
 4    Text(
 5        text = "Con padding fuera del fondo",
 6        modifier = Modifier
 7            .background(Color.Yellow)   // el fondo NO incluye el padding
 8            .padding(16.dp)             // el padding se aplica DENTRO del fondo
 9    )
10
11    Spacer(modifier = Modifier.height(16.dp))
12
13    Text(
14        text = "Con fondo sobre el padding",
15        modifier = Modifier
16            .padding(16.dp)             // el padding se aplica ANTES del fondo
17            .background(Color.Yellow)   // el fondo SÍ incluye el padding
18    )
19}

Modificadores más usados#

 1Modifier
 2    // Tamaño
 3    .fillMaxSize()              // ocupa todo el espacio disponible
 4    .fillMaxWidth()             // ancho máximo
 5    .fillMaxHeight()            // alto máximo
 6    .size(100.dp)               // tamaño fijo
 7    .width(200.dp)
 8    .height(50.dp)
 9    .wrapContentSize()          // se ajusta al contenido
10
11    // Espaciado
12    .padding(16.dp)             // todos los lados
13    .padding(horizontal = 16.dp, vertical = 8.dp)
14    .padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp)
15
16    // Apariencia
17    .background(Color.Blue)
18    .background(MaterialTheme.colorScheme.surface)
19    .clip(RoundedCornerShape(8.dp))    // esquinas redondeadas
20    .clip(CircleShape)
21    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
22    .alpha(0.5f)                       // opacidad
23
24    // Interacción
25    .clickable { /* acción */ }
26    .clickable(enabled = condicion) { /* acción */ }
27
28    // Posicionamiento
29    .offset(x = 8.dp, y = 4.dp)
30    .align(Alignment.CenterHorizontally)   // dentro de Column
31    .weight(1f)                            // en Row o Column: ocupa el espacio proporcional

8. Estado y recomposición 🔑#

El estado en Compose es cualquier valor cuyo cambio debe provocar una actualización de la UI. Este es el concepto más importante de Compose: la UI es siempre una función del estado.

UI = f(Estado)

remember y mutableStateOf#

 1@Composable
 2fun ContadorCompleto() {
 3    // remember: guarda el valor entre recomposiciones
 4    // mutableStateOf: crea un observable que Compose vigila
 5    // by: delegación de propiedades (by remember { ... } vs = remember { ... })
 6    var contador by remember { mutableIntStateOf(0) }   // optimizado para Int
 7
 8    Column(
 9        modifier = Modifier.fillMaxSize(),
10        horizontalAlignment = Alignment.CenterHorizontally,
11        verticalArrangement = Arrangement.Center
12    ) {
13        Text(
14            text = "Contador: $contador",
15            style = MaterialTheme.typography.displayMedium
16        )
17        Spacer(modifier = Modifier.height(24.dp))
18        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
19            Button(
20                onClick = { if (contador > 0) contador-- },
21                enabled = contador > 0
22            ) { Text("−") }
23
24            Button(onClick = { contador++ }) { Text("+") }
25        }
26    }
27}

¿Por qué remember?#

Sin remember, el estado se reinicializaría en cada recomposición (cada vez que Compose redibuja el Composable). Con remember, el valor sobrevive a las recomposiciones:

 1@Composable
 2fun DemoSinRemember() {
 3    var x = 0   // ⚠️ se reinicia a 0 en cada recomposición
 4    Button(onClick = { x++ }) { Text("x = $x") }
 5    // x nunca supera 0: se reinicia antes de mostrarse
 6}
 7
 8@Composable
 9fun DemoConRemember() {
10    var x by remember { mutableIntStateOf(0) }   // ✅ persiste entre recomposiciones
11    Button(onClick = { x++ }) { Text("x = $x") }
12}

remember solo sobrevive a recomposiciones. Si la Activity se destruye (rotación de pantalla, proceso killed), el estado se pierde. Para que el estado sobreviva a estos eventos, se usa rememberSaveable o, en arquitectura MVVM, el ViewModel (Bloque 2).

rememberSaveable#

 1@Composable
 2fun DemoRememberSaveable() {
 3    // Sobrevive a rotación de pantalla y a death del proceso
 4    var texto by rememberSaveable { mutableStateOf("") }
 5
 6    OutlinedTextField(
 7        value = texto,
 8        onValueChange = { texto = it },
 9        label = { Text("Persiste al rotar") }
10    )
11}

Estado derivado#

 1@Composable
 2fun ListaConFiltro() {
 3    val peliculas = remember {
 4        listOf("Dune", "Inception", "Interstellar", "Dune: Parte 2", "Morbius")
 5    }
 6    var filtro by remember { mutableStateOf("") }
 7
 8    // derivedStateOf: recalcula solo cuando cambien sus dependencias
 9    val peliculasFiltradas by remember(filtro) {
10        derivedStateOf {
11            if (filtro.isBlank()) peliculas
12            else peliculas.filter { it.contains(filtro, ignoreCase = true) }
13        }
14    }
15
16    Column(modifier = Modifier.padding(16.dp)) {
17        OutlinedTextField(
18            value = filtro,
19            onValueChange = { filtro = it },
20            label = { Text("Filtrar") },
21            modifier = Modifier.fillMaxWidth()
22        )
23        Spacer(modifier = Modifier.height(8.dp))
24        peliculasFiltradas.forEach { pelicula ->
25            Text(pelicula, modifier = Modifier.padding(vertical = 4.dp))
26        }
27    }
28}

Puedes ampliar información sobre remember y otras funciones de estado en la documentación oficial y en el Anexo 2 de este tema.


9. Listas eficientes: LazyColumn y LazyRow 🔑#

Para listas con muchos elementos, Compose proporciona composables “perezosos” que solo renderizan los elementos visibles, equivalentes al RecyclerView del sistema clásico:

 1@Composable
 2fun ListaPeliculas(peliculas: List<String>) {
 3    LazyColumn(
 4        contentPadding = PaddingValues(16.dp),          // padding exterior
 5        verticalArrangement = Arrangement.spacedBy(8.dp) // espacio entre elementos
 6    ) {
 7        // items con lista
 8        items(
 9            items = peliculas,
10            key = { pelicula -> pelicula }   // clave única para optimizar recomposiciones
11        ) { pelicula ->
12            TarjetaPelicula(pelicula)
13        }
14
15        // item individual (cabecera, separador, etc.)
16        item {
17            Divider()
18            Text(
19                text = "${peliculas.size} películas en total",
20                style = MaterialTheme.typography.bodySmall,
21                modifier = Modifier.padding(8.dp)
22            )
23        }
24    }
25}
26
27// Elemento individual de la lista
28@Composable
29fun TarjetaPelicula(titulo: String) {
30    Card(
31        modifier = Modifier.fillMaxWidth(),
32        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
33    ) {
34        Row(
35            modifier = Modifier
36                .fillMaxWidth()
37                .padding(16.dp),
38            verticalAlignment = Alignment.CenterVertically
39        ) {
40            Icon(
41                imageVector = Icons.Default.Movie,
42                contentDescription = null,
43                tint = MaterialTheme.colorScheme.primary
44            )
45            Spacer(modifier = Modifier.width(12.dp))
46            Text(
47                text = titulo,
48                style = MaterialTheme.typography.bodyLarge,
49                modifier = Modifier.weight(1f)
50            )
51            Icon(
52                imageVector = Icons.Default.ChevronRight,
53                contentDescription = "Ver detalle"
54            )
55        }
56    }
57}

LazyRow — lista horizontal#

 1@Composable
 2fun CarruselGeneros(generos: List<String>, seleccionado: String, onSeleccion: (String) -> Unit) {
 3    LazyRow(
 4        horizontalArrangement = Arrangement.spacedBy(8.dp),
 5        contentPadding = PaddingValues(horizontal = 16.dp)
 6    ) {
 7        items(generos) { genero ->
 8            FilterChip(
 9                selected = genero == seleccionado,
10                onClick = { onSeleccion(genero) },
11                label = { Text(genero) }
12            )
13        }
14    }
15}

10. Scaffold: estructura básica de pantalla 🔑#

Scaffold implementa la estructura visual estándar de Material Design: barra superior, barra inferior, botón de acción flotante y contenido principal.

 1@Composable
 2fun AppFlixScreen() {
 3    // snackbarHostState gestiona los mensajes temporales (Snackbar)
 4    val snackbarHostState = remember { SnackbarHostState() }
 5    val scope = rememberCoroutineScope()
 6
 7    Scaffold(
 8        topBar = {
 9            TopAppBar(
10                title = { Text("AppFlix") },
11                navigationIcon = {
12                    IconButton(onClick = { /* abrir menú o navegar atrás */ }) {
13                        Icon(Icons.Default.Menu, contentDescription = "Menú")
14                    }
15                },
16                actions = {
17                    IconButton(onClick = { /* búsqueda */ }) {
18                        Icon(Icons.Default.Search, contentDescription = "Buscar")
19                    }
20                },
21                colors = TopAppBarDefaults.topAppBarColors(
22                    containerColor = MaterialTheme.colorScheme.primaryContainer
23                )
24            )
25        },
26        floatingActionButton = {
27            FloatingActionButton(onClick = { /* añadir */ }) {
28                Icon(Icons.Default.Add, contentDescription = "Añadir película")
29            }
30        },
31        snackbarHost = { SnackbarHost(snackbarHostState) }
32    ) { paddingValues ->
33        // paddingValues contiene el espacio ocupado por TopAppBar y FAB
34        // SIEMPRE debe aplicarse al contenido principal
35        LazyColumn(contentPadding = paddingValues) {
36            items(10) { index ->
37                TarjetaPelicula("Película $index")
38            }
39        }
40    }
41}

11. Diálogos y notificaciones#

AlertDialog#

 1@Composable
 2fun EjemploDialogo() {
 3    var mostrarDialogo by remember { mutableStateOf(false) }
 4
 5    Button(onClick = { mostrarDialogo = true }) {
 6        Text("Eliminar favorito")
 7    }
 8
 9    if (mostrarDialogo) {
10        AlertDialog(
11            onDismissRequest = { mostrarDialogo = false },
12            title = { Text("Confirmar eliminación") },
13            text = { Text("¿Seguro que quieres eliminar esta película de favoritos?") },
14            confirmButton = {
15                TextButton(onClick = {
16                    mostrarDialogo = false
17                    /* acción de eliminación */
18                }) { Text("Eliminar") }
19            },
20            dismissButton = {
21                TextButton(onClick = { mostrarDialogo = false }) {
22                    Text("Cancelar")
23                }
24            }
25        )
26    }
27}

Snackbar#

 1@Composable
 2fun EjemploSnackbar() {
 3    val snackbarHostState = remember { SnackbarHostState() }
 4    val scope = rememberCoroutineScope()
 5
 6    Scaffold(
 7        snackbarHost = { SnackbarHost(snackbarHostState) }
 8    ) { padding ->
 9        Column(modifier = Modifier.padding(padding)) {
10            Button(
11                onClick = {
12                    scope.launch {
13                        val resultado = snackbarHostState.showSnackbar(
14                            message = "Película añadida a favoritos",
15                            actionLabel = "Deshacer",
16                            duration = SnackbarDuration.Short
17                        )
18                        if (resultado == SnackbarResult.ActionPerformed) {
19                            // El usuario pulsó "Deshacer"
20                        }
21                    }
22                }
23            ) { Text("Añadir a favoritos") }
24        }
25    }
26}

12. Previsualizaciones en Android Studio#

Las anotaciones @Preview permiten ver el Composable directamente en el editor sin ejecutar la app. Puedes tener múltiples previsualizaciones en el mismo archivo:

 1// Previsualización básica
 2@Preview(showBackground = true)
 3@Composable
 4fun TarjetaPeliculaPreview() {
 5    AppTheme {
 6        TarjetaPelicula("Dune: Parte 2")
 7    }
 8}
 9
10// Previsualización en modo oscuro
11@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES, name = "Modo oscuro")
12@Composable
13fun TarjetaPeliculaDarkPreview() {
14    AppTheme {
15        TarjetaPelicula("Dune: Parte 2")
16    }
17}
18
19// Previsualización con tamaño de dispositivo específico
20@Preview(
21    showBackground = true,
22    device = "spec:width=411dp,height=891dp",
23    name = "Pixel 5"
24)
25@Composable
26fun PantallaCompletaPreview() {
27    AppTheme {
28        AppFlixScreen()
29    }
30}

Ejemplo práctico completo: pantalla de bienvenida de AppFlix#

Este ejemplo integra todos los conceptos de la sección en una pantalla autocontenida y funcional:

 1package com.ejemplo.appflix
 2
 3import androidx.compose.foundation.layout.*
 4import androidx.compose.foundation.shape.RoundedCornerShape
 5import androidx.compose.material.icons.Icons
 6import androidx.compose.material.icons.filled.Movie
 7import androidx.compose.material3.*
 8import androidx.compose.runtime.*
 9import androidx.compose.ui.Alignment
10import androidx.compose.ui.Modifier
11import androidx.compose.ui.text.style.TextAlign
12import androidx.compose.ui.tooling.preview.Preview
13import androidx.compose.ui.unit.dp
14
15// ─── PantallaBienvenida.kt ───────────────────────────────────────────────────────────────────────
16@Composable
17fun PantallaBienvenida(onEntrar: () -> Unit) {
18    var nombreUsuario by remember { mutableStateOf("") }
19    val botonHabilitado = nombreUsuario.trim().length >= 3
20
21    Column(
22        modifier = Modifier
23            .fillMaxSize()
24            .padding(32.dp),
25        horizontalAlignment = Alignment.CenterHorizontally,
26        verticalArrangement = Arrangement.Center
27    ) {
28        // Icono y título
29        Icon(
30            imageVector = Icons.Default.Movie,
31            contentDescription = null,
32            modifier = Modifier.size(80.dp),
33            tint = MaterialTheme.colorScheme.primary
34        )
35        Spacer(modifier = Modifier.height(16.dp))
36        Text(
37            text = "AppFlix",
38            style = MaterialTheme.typography.displayMedium,
39            color = MaterialTheme.colorScheme.primary
40        )
41        Text(
42            text = "Tu catálogo de películas",
43            style = MaterialTheme.typography.bodyLarge,
44            color = MaterialTheme.colorScheme.onSurfaceVariant,
45            textAlign = TextAlign.Center
46        )
47
48        Spacer(modifier = Modifier.height(48.dp))
49
50        // Campo de nombre
51        OutlinedTextField(
52            value = nombreUsuario,
53            onValueChange = { nombreUsuario = it },
54            label = { Text("¿Cómo te llamas?") },
55            singleLine = true,
56            modifier = Modifier.fillMaxWidth(),
57            supportingText = {
58                Text("Mínimo 3 caracteres (${nombreUsuario.length}/3)")
59            }
60        )
61
62        Spacer(modifier = Modifier.height(24.dp))
63
64        // Botón de acceso
65        Button(
66            onClick = onEntrar,
67            enabled = botonHabilitado,
68            modifier = Modifier
69                .fillMaxWidth()
70                .height(56.dp),
71            shape = RoundedCornerShape(8.dp)
72        ) {
73            Text(
74                text = if (botonHabilitado) "Entrar como ${nombreUsuario.trim()}" else "Entrar",
75                style = MaterialTheme.typography.labelLarge
76            )
77        }
78    }
79}
80
81@Preview(showBackground = true)
82@Composable
83fun PantallaBienvenidaPreview() {
84    MaterialTheme {
85        PantallaBienvenida(onEntrar = { })
86    }
87}

Este ejemplo hace uso de la dependencia de Material3 de Icons extendida, por lo que es necesario añadirla al build.gradle:

1// Material Icons
2implementation("androidx.compose.material:material-icons-extended:1.7.8")

Referencias#

Calendar  Última modificación: viernes, 26 de junio de 2026