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 ← dependenciasEl 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#
- Solo pueden llamarse desde otras funciones @Composable (o desde un ámbito de composición como
setContent). - No deben tener efectos secundarios directos: no modificar variables externas, no escribir en base de datos, no hacer llamadas de red directamente.
- Deben ser idempotentes: llamarlas varias veces con los mismos parámetros debe producir el mismo resultado.
- 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áticamenteCompose 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 proporcional8. 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}
remembersolo 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 usarememberSaveableo, en arquitectura MVVM, elViewModel(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
remembery 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")