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 AppDummyApp()
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 AppDummy", // 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 AppDummyScreen() {
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("AppDummy") },
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 AppDummyScreen()
29 }
30}Ejemplo práctico completo: pantalla de bienvenida de AppDummy#
Este ejemplo integra todos los conceptos de la sección en una pantalla autocontenida y totalmente funcional. Se partirá de un proyecto nuevo de Android Studio con plantilla Empty Activity, o puedes hacer uso del proyecto ya creado v0. Creación inicial del proyecto AppDummy
. Se añadirá el siguiente Composable, creando para ello un nuevo archivo PantallaBienvenida.kt en el paquete com.ejemplo.appdummy:
Si haces uso del proyecto de ejemplo, verás que el nombre del paquete es
es.javiercarrasco.appdummy. Puedes cambiarlo acom.ejemplo.appdummyo con tu nombre si lo deseas, pero no es necesario para que funcione.
1package com.ejemplo.appdummy
2
3import androidx.compose.foundation.layout.*
4import androidx.compose.foundation.shape.RoundedCornerShape
5import androidx.compose.material.icons.Icons
6import androidx.compose.material.icons.filled.Book
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.Book,
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 = "AppDummy",
38 style = MaterialTheme.typography.displayMedium,
39 color = MaterialTheme.colorScheme.primary
40 )
41 Text(
42 text = "Tu catálogo de libros",
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 Material de Icons extendida, por lo que es necesario añadirla al
build.gradle.kts(:app):1// Material Icons Extended 2implementation("androidx.compose.material:material-icons-extended:1.7.8")O en su versión de catálogo de dependencias (TOML), añandiendo la siguiente línea al archivo
libs.versions.toml:1androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }Y luego en el
build.gradle.kts(:app):1// Material Icons Extended 2implementation("androidx.compose.material:material-icons-extended")