Jetpack ComposeCheatsheet en español
Android · Kotlin · Material 3

Jetpack Compose Cheatsheet

Guía rápida para crear interfaces en Android con Jetpack Compose: desde los modificadores y layouts básicos hasta el estado, el ViewModel y la navegación. Con ejemplos listos para copiar.

01 Fundamentos

En Compose la interfaz se escribe con funciones de Kotlin. Cada función describe cómo se ve una parte de la pantalla según los datos que recibe.

@Composable y setContent

Una función marcada con @Composable pinta interfaz. Su nombre empieza por mayúscula (como una clase) y no devuelve nada. La Activity carga la primera pantalla con setContent.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MiAppTheme {
                Saludo(nombre = "Eva")
            }
        }
    }
}

@Composable
fun Saludo(nombre: String, modifier: Modifier = Modifier) {
    Text(text = "Hola, $nombre", modifier = modifier)
}

Buena práctica: que cada composable reciba un modifier: Modifier = Modifier como primer parámetro opcional y lo aplique a su elemento raíz. Así quien lo usa puede darle tamaño, márgenes o posición.

@Preview

Muestra el composable en el panel de diseño de Android Studio sin ejecutar la app.

@Preview(showBackground = true, name = "Claro")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Oscuro")
@Composable
fun SaludoPreview() {
    MiAppTheme {
        Saludo(nombre = "Android")
    }
}

Recomposición

Cuando cambia un dato que la interfaz está leyendo (un estado), Compose vuelve a ejecutar solo los composables afectados. A esto se le llama recomposición.

ReglaPor qué
No guardes estado en variables normalesSe pierden en cada recomposición. Usa remember { mutableStateOf(...) }.
Sin efectos secundarios en el cuerpoUn composable puede ejecutarse muchas veces. Las llamadas a red o base de datos van en un efecto o en el ViewModel.
Los datos bajan, los eventos subenEl padre pasa valores al hijo y el hijo avisa con lambdas (onClick, onValueChange…).

02 Modifier

Los modificadores se encadenan para definir el tamaño, el aspecto y el comportamiento de los composables.

Tamaño y espacio

ModificadorDescripción
fillMaxSize()Ocupa todo el espacio disponible
fillMaxWidth()Se estira horizontalmente todo lo posible
fillMaxWidth(0.5f)Ocupa la mitad del ancho disponible
height(50.dp)Altura específica
width(140.dp)Ancho específico
size(140.dp, 180.dp)Ancho y alto juntos (size(48.dp) para un cuadrado)
padding(24.dp)Margen interno en los cuatro lados
padding(horizontal = 16.dp, vertical = 8.dp)Margen distinto en horizontal y vertical
aspectRatio(16f / 9f)Mantiene una proporción ancho/alto
weight(1f)Reparte el espacio sobrante de forma proporcional. Solo dentro de Row o Column
align(Alignment.Center)Mueve un único elemento dentro de su padre (en Box; en Row se usa Alignment.CenterVertically)
offset(x = 8.dp, y = 4.dp)Desplaza el elemento sin cambiar el sitio que ocupa

Aspecto

ModificadorDescripción
background(colorResource(R.color.BlancoGrisaceo))Color de fondo desde res/values/colors.xml
background(Color.LightGray, RoundedCornerShape(12.dp))Fondo con forma
clip(RoundedCornerShape(12.dp))Recorta el contenido con esa forma (CircleShape para círculos)
border(1.dp, Color.Gray, RoundedCornerShape(8.dp))Borde alrededor
shadow(6.dp, RoundedCornerShape(12.dp))Sombra con forma
alpha(0.5f)Transparencia (de 0.0 a 1.0)

Interacción y scroll

ModificadorDescripción
clickable { navController.navigate("ProductHistory") }Pulsable (aquí, navega a otra pantalla)
verticalScroll(rememberScrollState())Permite hacer scroll en una Column con poco contenido
horizontalScroll(rememberScrollState())Scroll horizontal en una Row
semantics { contentDescription = "…" }Información para lectores de pantalla

Ojo: para listas largas no uses verticalScroll, usa LazyColumn: solo crea los elementos que se ven en pantalla.

El orden importa

Cada modificador se aplica sobre el resultado del anterior. Cambiar el orden cambia el resultado:

// El fondo cubre también el padding: caja azul grande con el texto dentro
Modifier.background(Color.Blue).padding(16.dp)

// El padding va primero: queda un margen transparente alrededor del fondo
Modifier.padding(16.dp).background(Color.Blue)

// clip antes de clickable: la onda del clic respeta las esquinas redondeadas
Modifier.clip(RoundedCornerShape(12.dp)).clickable { }

03 Layouts y alineación

Row coloca en horizontal, Column en vertical y Box apila unos encima de otros.

Row()

Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
) { /* elementos */ }

Column()

Column(
    modifier = Modifier.fillMaxHeight(),
    verticalArrangement = Arrangement.Top,
    horizontalAlignment = Alignment.CenterHorizontally
) { /* elementos */ }

horizontalArrangement Row

Cómo se reparten los elementos a lo largo de la fila.

ValorComportamiento
StartAlinea los elementos al principio (izquierda)
EndAlinea los elementos al final (derecha)
CenterLos agrupa todos en el centro horizontal
SpaceBetweenPrimero y último en los bordes; espacio restante en medio
SpaceAroundEspacio uniforme; los extremos con la mitad de espacio
SpaceEvenlyEspacio exactamente igual entre elementos y bordes
spacedBy(8.dp)Hueco fijo de 8dp entre cada elemento

verticalArrangement Column

Cómo se reparten los elementos a lo largo de la columna.

ValorComportamiento
TopAlinea los elementos en la parte superior
BottomAlinea los elementos en la parte inferior
CenterLos agrupa todos en el centro vertical
SpaceBetweenPrimero arriba, último abajo; espacio repartido en medio
SpaceAroundEspacio uniforme (los extremos con la mitad de espacio)
SpaceEvenlyEspacio idéntico en todas partes
spacedBy(16.dp)Hueco fijo de 16dp entre cada elemento

verticalAlignment Row

Posición en el otro eje (vertical) de una fila.

ValorEfecto
TopBorde superior
CenterVerticallyCentra verticalmente
BottomBorde inferior

horizontalAlignment Column

Posición en el otro eje (horizontal) de una columna.

ValorEfecto
StartAl inicio (izquierda)
CenterHorizontallyCentra horizontalmente
EndAl final (derecha)

Box() y contentAlignment

Los hijos se apilan; el último queda encima. Para colocarlos se usa contentAlignment en el Box o Modifier.align() en cada hijo:

PropiedadDescripción
contentAlignment = Alignment.CenterAlinea los hijos en el centro (por defecto TopStart)
propagateMinConstraints = trueObliga a los hijos a medir al menos lo mismo que el Box
Modifier.matchParentSize() (hijos)El hijo mide exactamente lo mismo que el Box
Box(modifier = Modifier.size(120.dp)) {
    Image(painterResource(R.drawable.foto), contentDescription = null, modifier = Modifier.matchParentSize())
    Text("Oferta", modifier = Modifier.align(Alignment.TopEnd).padding(8.dp))
}

Spacer()

ModificadorUso
height(16.dp)Espacio vertical (en Column)
width(8.dp)Espacio horizontal (en Row)
size(20.dp)Cuadrado invisible de tamaño fijo
weight(1f)Truco clave: empuja los elementos a extremos opuestos

contentPadding = PaddingValues()

Margen interior de listas, botones y Scaffold.

UsoSintaxis
Igual en los 4 ladosPaddingValues(16.dp)
Horizontal / verticalPaddingValues(horizontal = 16.dp, vertical = 8.dp)
Por ladoPaddingValues(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 80.dp)

04 Listas y grids

Las listas Lazy solo crean los elementos visibles, así que sirven para cientos o miles de filas.

LazyColumn / LazyRow Nuevo

LazyColumn(
    modifier = Modifier.fillMaxSize(),
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
) {
    item {
        Text("Cabecera", style = MaterialTheme.typography.titleLarge)
    }
    items(productos, key = { it.id }) { producto ->
        ProductoCard(producto)
    }
}
FunciónPara qué
item { }Un único elemento (cabecera, pie…)
items(lista) { }Un elemento por cada objeto de la lista
itemsIndexed(lista) { i, x -> }Igual, pero con la posición
key = { it.id }Identifica cada fila: evita fallos al borrar o reordenar
stickyHeader { }Cabecera que se queda fija al hacer scroll
state = rememberLazyListState()Para saber o cambiar la posición del scroll

Import: items(lista) necesita import androidx.compose.foundation.lazy.items. Si Android Studio marca error con una lista, suele ser esto.

LazyVerticalGrid()

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp),
    contentPadding = PaddingValues(8.dp),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
) {
    items(fotos) { foto -> FotoItem(foto) }
}
PropiedadDescripción
columns = GridCells.Fixed(3)Exactamente 3 columnas
columns = GridCells.Adaptive(128.dp)Tantas columnas como quepan, de 128dp como mínimo
span = { GridItemSpan(maxLineSpan) }Un elemento que ocupa toda la fila (en item)

05 Texto y campos

Text()

PropiedadDescripciónEjemplo
textEl contenido escrito"Hola"
colorColor del textoColor.Red
fontSizeTamaño de letra (usa .sp)20.sp
fontStyleEstilo de fuenteFontStyle.Italic
fontWeightGrosor de la letraFontWeight.Bold
fontFamilyTipo de fuenteFontFamily.Monospace
letterSpacingEspacio entre letras2.sp
textDecorationSubrayado / tachadoTextDecoration.Underline
textAlignAlineación del textoTextAlign.Center
lineHeightAltura de línea30.sp
overflowPuntos suspensivos si no cabeTextOverflow.Ellipsis
maxLinesLímite máximo de líneas2
styleEstilo predefinido del temaMaterialTheme.typography.bodyLarge

Textos en recursos: Text(stringResource(R.string.titulo)) lee de strings.xml y permite traducir la app.

TextField / OutlinedTextField

TextField tiene fondo relleno y OutlinedTextField solo borde. Los dos se usan igual y necesitan un estado donde guardar lo escrito.

PropiedadDescripción
value = textStateValor actual del texto (estado con remember)
onValueChange = { textState = it }Se ejecuta al escribir y actualiza el estado
label = { Text("Nombre") }Texto flotante indicador
placeholder = { Text("Ej: Juan") }Texto de pista (desaparece al escribir)
leadingIcon = { Icon(...) }Icono al inicio del campo
trailingIcon = { Icon(...) }Icono al final del campo
supportingText = { Text("Obligatorio") }Texto de ayuda o de error debajo del campo
isError = trueBorde rojo si hay error
visualTransformation = PasswordVisualTransformation()Oculta el texto (contraseña)
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)Tipo de teclado
keyboardActions = KeyboardActions(onDone = { ... })Acción al pulsar Intro
singleLine = trueFuerza una sola línea
maxLines = 2Máximo de líneas antes del scroll interno
shape = RoundedCornerShape(12.dp)Redondeo de esquinas

Buscador Ejemplo

@Composable
fun SearchField(label: String, modifier: Modifier = Modifier) {
    var search by remember { mutableStateOf("") }

    TextField(
        value = search,
        onValueChange = { search = it },
        label = { Text(text = label) },
        leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
        singleLine = true,
        modifier = modifier
    )
}

Para filtrar una lista con lo que se escribe, el texto tiene que estar en el padre: mira la elevación de estado y el ejemplo completo.

06 Botones y controles

Button()

TipoDescripción
ButtonRelleno sólido (acción principal)
FilledTonalButtonFondo con color suave (tonal)
ElevatedButtonCon sombra
OutlinedButtonBorde sin fondo (acción secundaria)
TextButtonSolo texto, sin bordes ni fondo
IconButtonBotón que solo lleva un icono
FloatingActionButtonBotón flotante para la acción principal de la pantalla

Propiedades comunes

PropiedadDescripción
onClick = { /* Acción */ }Código al pulsar
enabled = truefalse → gris e inactivo
shape = RoundedCornerShape(8.dp)Forma de las esquinas
elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp)Sombra del botón
colors = ButtonDefaults.buttonColors(containerColor = ..., contentColor = ...)Colores de fondo y contenido
border = BorderStroke(1.dp, Color.Blue)Borde alrededor
contentPadding = PaddingValues(16.dp)Margen interno
Button(onClick = { guardar() }) {
    Icon(Icons.Default.Check, contentDescription = null)
    Spacer(Modifier.width(8.dp))
    Text("Guardar")
}

Checkbox, Switch y RadioButton Nuevo

Funcionan igual que un campo de texto: un estado con el valor y una lambda que lo cambia.

var aceptado by remember { mutableStateOf(false) }
var notificaciones by remember { mutableStateOf(true) }

Row(verticalAlignment = Alignment.CenterVertically) {
    Checkbox(checked = aceptado, onCheckedChange = { aceptado = it })
    Text("Acepto las condiciones")
}

Switch(checked = notificaciones, onCheckedChange = { notificaciones = it })

val opciones = listOf("Pequeño", "Mediano", "Grande")
var elegida by remember { mutableStateOf(opciones[0]) }
opciones.forEach { opcion ->
    Row(
        Modifier.fillMaxWidth().selectable(selected = opcion == elegida, onClick = { elegida = opcion }),
        verticalAlignment = Alignment.CenterVertically
    ) {
        RadioButton(selected = opcion == elegida, onClick = null)
        Text(opcion)
    }
}

Fila entera pulsable: con Modifier.selectable en la fila y onClick = null en el RadioButton, el usuario puede tocar también el texto.

Slider Nuevo

var volumen by remember { mutableFloatStateOf(5f) }

Slider(
    value = volumen,
    onValueChange = { volumen = it },
    valueRange = 0f..10f,
    steps = 9   // 9 paradas intermedias: valores enteros del 0 al 10
)
Text("Volumen: ${volumen.toInt()}")

07 Card, Scaffold y diálogos

Card()

PropiedadDescripción
modifier = Modifier.padding(16.dp)Margen externo
shape = RoundedCornerShape(12.dp)Curvatura de las esquinas
colors = CardDefaults.cardColors(containerColor = ...)Color de fondo
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)Tamaño de la sombra
border = BorderStroke(1.dp, Color.Black)Borde (lo trae por defecto OutlinedCard)
onClick = { ... }Hace la tarjeta pulsable

También existen ElevatedCard (con sombra) y OutlinedCard (con borde). Surface es la versión más básica: un fondo con color, forma y sombra.

Scaffold con barra superior y botón flotante Nuevo

Es el esqueleto de una pantalla: coloca la barra superior, la inferior, el botón flotante y los avisos, y te pasa el espacio que queda libre.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PantallaLista() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Mis tareas") },
                actions = {
                    IconButton(onClick = { /* ajustes */ }) {
                        Icon(Icons.Default.Settings, contentDescription = "Ajustes")
                    }
                }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { /* nueva tarea */ }) {
                Icon(Icons.Default.Add, contentDescription = "Añadir")
            }
        }
    ) { innerPadding ->
        LazyColumn(contentPadding = innerPadding) {
            // ...
        }
    }
}

No ignores innerPadding: si no lo aplicas, el contenido queda tapado por las barras.

ParámetroQué va dentro
topBarTopAppBar, CenterAlignedTopAppBar…
bottomBarNavigationBar con NavigationBarItem (pestañas inferiores)
floatingActionButtonFloatingActionButton o ExtendedFloatingActionButton
snackbarHostSnackbarHost(snackbarHostState) para avisos temporales

AlertDialog Nuevo

El diálogo se muestra mientras una condición sea verdadera.

var mostrarDialogo by remember { mutableStateOf(false) }

if (mostrarDialogo) {
    AlertDialog(
        onDismissRequest = { mostrarDialogo = false },
        title = { Text("¿Borrar tarea?") },
        text = { Text("Esta acción no se puede deshacer.") },
        confirmButton = {
            TextButton(onClick = { borrar(); mostrarDialogo = false }) { Text("Borrar") }
        },
        dismissButton = {
            TextButton(onClick = { mostrarDialogo = false }) { Text("Cancelar") }
        }
    )
}

Snackbar Nuevo

Aviso breve en la parte inferior. Se muestra desde una corrutina, porque espera a que desaparezca.

val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { innerPadding ->
    Button(
        onClick = { scope.launch { snackbarHostState.showSnackbar("Guardado") } },
        modifier = Modifier.padding(innerPadding)
    ) { Text("Guardar") }
}

08 Iconos e imágenes

Icon()

PropiedadDescripción
imageVector = Icons.Default.AddDibujo del icono (estilos: Default, Rounded, Outlined, Sharp, TwoTone)
contentDescription = "Añadir"Para lectores de pantalla. null si es decorativo
tint = Color.RedColor del icono
modifier = Modifier.size(24.dp)Tamaño (el estándar es 24dp)
painter = painterResource(R.drawable.mi_icono)Icono propio desde res/drawable

¿Faltan iconos? El paquete básico trae pocos. Para tener todos, añade la dependencia androidx.compose.material:material-icons-extended, o importa iconos sueltos desde Android Studio (New → Vector Asset).

Image()

PropiedadDescripción
painter = painterResource(R.drawable.mi_imagen)Carga desde res/drawable
contentDescription = "Descripción"Accesibilidad
modifier = Modifier.clip(CircleShape)Recorta (por ejemplo, en círculo)
contentScale = ContentScale.CropAjuste al contenedor: Crop recorta, Fit encaja sin recortar
alpha = 0.5fTransparencia (de 0.0 a 1.0)

Imágenes de internet con Coil Nuevo

Compose no descarga imágenes por sí solo. La librería más usada es Coil: añade io.coil-kt.coil3:coil-compose y io.coil-kt.coil3:coil-network-okhttp, y el permiso de internet en el AndroidManifest.xml.

AsyncImage(
    model = producto.urlFoto,
    contentDescription = producto.nombre,
    contentScale = ContentScale.Crop,
    placeholder = painterResource(R.drawable.cargando),
    error = painterResource(R.drawable.sin_foto),
    modifier = Modifier.size(80.dp).clip(RoundedCornerShape(8.dp))
)
<uses-permission android:name="android.permission.INTERNET" />

09 Estado Nuevo

El estado es cualquier dato que puede cambiar y que la pantalla muestra. Cuando cambia, Compose repinta lo necesario.

remember + mutableStateOf

@Composable
fun Contador() {
    var contador by remember { mutableStateOf(0) }

    Button(onClick = { contador++ }) {
        Text("Pulsado $contador veces")
    }
}
PiezaQué hace
mutableStateOf(0)Crea un valor observable: al cambiarlo, se repinta lo que lo lee
remember { }Lo conserva entre recomposiciones
byPermite usar contador directamente en lugar de contador.value
rememberSaveable { }Igual que remember, pero sobrevive al girar la pantalla

Si by da error, faltan estos imports: import androidx.compose.runtime.getValue y import androidx.compose.runtime.setValue.

Tipos de estado

FunciónPara qué
mutableStateOf("")Cualquier valor (texto, booleano, objeto…)
mutableIntStateOf(0) / mutableFloatStateOf(0f)Números, sin conversiones internas
mutableStateListOf<Tarea>()Lista observable: avisa al añadir o borrar elementos
derivedStateOf { }Valor calculado a partir de otros estados, que solo cambia cuando cambia el resultado

Error típico: mutableStateOf(mutableListOf()) y luego lista.add(x) no repinta, porque la lista es la misma. Usa mutableStateListOf() o crea una lista nueva: lista = lista + x.

Elevación de estado (state hoisting)

Si dos composables necesitan el mismo dato, el estado se sube al padre común. El hijo recibe el valor y un evento para cambiarlo, así no guarda nada y es fácil de reutilizar y probar.

// Hijo sin estado: solo muestra y avisa
@Composable
fun Buscador(texto: String, onTextoChange: (String) -> Unit) {
    OutlinedTextField(value = texto, onValueChange = onTextoChange, label = { Text("Buscar") })
}

// Padre con el estado: lo comparte con el buscador y con la lista
@Composable
fun PantallaProductos(productos: List<Producto>) {
    var texto by rememberSaveable { mutableStateOf("") }
    val filtrados = productos.filter { it.nombre.contains(texto, ignoreCase = true) }

    Column {
        Buscador(texto = texto, onTextoChange = { texto = it })
        ListaProductos(filtrados)
    }
}

10 ViewModel Nuevo

El ViewModel guarda el estado de una pantalla fuera de la interfaz: sobrevive a los giros y separa la lógica de lo que se pinta (patrón MVVM).

ViewModel + StateFlow

data class TareasUiState(
    val tareas: List<Tarea> = emptyList(),
    val cargando: Boolean = false
)

class TareasViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(TareasUiState())
    val uiState: StateFlow<TareasUiState> = _uiState.asStateFlow()

    fun anadir(titulo: String) {
        _uiState.update { estado ->
            estado.copy(tareas = estado.tareas + Tarea(titulo))
        }
    }
}

La pantalla obtiene el ViewModel y lee su estado:

@Composable
fun PantallaTareas(viewModel: TareasViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    LazyColumn {
        items(uiState.tareas) { tarea -> Text(tarea.titulo) }
    }
}
PiezaQué hace
_uiState privadoSolo el ViewModel puede cambiar el estado
uiState públicoLa pantalla solo puede leerlo
update { it.copy(...) }Crea un estado nuevo a partir del anterior
viewModel()Crea el ViewModel o reutiliza el que ya existe
collectAsStateWithLifecycle()Convierte el StateFlow en estado de Compose y deja de escuchar cuando la app está en segundo plano

Dependencias: androidx.lifecycle:lifecycle-viewmodel-compose (para viewModel()) y androidx.lifecycle:lifecycle-runtime-compose (para collectAsStateWithLifecycle()).

Operaciones lentas: viewModelScope

Las llamadas a red o a base de datos se lanzan en una corrutina del ViewModel, que se cancela sola si se cierra la pantalla.

fun cargar() {
    viewModelScope.launch {
        _uiState.update { it.copy(cargando = true) }
        val tareas = repositorio.obtenerTareas()   // función suspend
        _uiState.update { it.copy(tareas = tareas, cargando = false) }
    }
}

12 Efectos Nuevo

Código que no pinta interfaz (cargar datos, mostrar avisos, empezar una animación) se ejecuta en un efecto para controlar cuándo y cuántas veces ocurre.

LaunchedEffect

Lanza una corrutina al aparecer el composable y la repite solo si cambia la clave.

@Composable
fun PantallaDetalle(id: Int, viewModel: DetalleViewModel = viewModel()) {
    LaunchedEffect(id) {
        viewModel.cargar(id)   // se ejecuta al entrar y cada vez que cambie id
    }
    // ...
}

rememberCoroutineScope

Para lanzar corrutinas desde un evento, como un onClick (por ejemplo, un Snackbar o un scroll animado).

val scope = rememberCoroutineScope()
val listState = rememberLazyListState()

Button(onClick = {
    scope.launch { listState.animateScrollToItem(0) }
}) { Text("Subir") }

DisposableEffect

Para registrar algo al entrar y limpiarlo al salir (sensores, listeners…).

DisposableEffect(Unit) {
    sensor.empezar()
    onDispose { sensor.parar() }
}

13 Tema y colores Nuevo

Android Studio crea un tema en ui/theme/ (Color.kt, Theme.kt, Type.kt). Usar los colores del tema en lugar de colores fijos hace que el modo oscuro funcione solo.

MaterialTheme

Text(
    text = "Título",
    style = MaterialTheme.typography.headlineSmall,
    color = MaterialTheme.colorScheme.primary
)

Surface(
    color = MaterialTheme.colorScheme.surfaceVariant,
    shape = MaterialTheme.shapes.medium
) { /* ... */ }
ColorUso
primary / onPrimaryColor de marca y el texto que va encima
secondary, tertiaryAcentos secundarios
background / onBackgroundFondo de la pantalla y su texto
surface / onSurfaceTarjetas, barras y hojas
surfaceVariantSuperficies con un tono distinto
errorMensajes y campos con error

Tipografía del tema

EstiloPara qué
displayLarge … displaySmallNúmeros o textos muy grandes
headlineLarge … headlineSmallTítulos de pantalla
titleLarge … titleSmallTítulos de tarjetas y secciones
bodyLarge … bodySmallTexto normal
labelLarge … labelSmallBotones, etiquetas y textos pequeños

colorResource o tema: colorResource(R.color.X) lee de colors.xml y está bien para un color concreto, pero no cambia con el modo oscuro. Para la app entera, define los colores en Theme.kt.

14 Ejemplo completo

Una lista de tareas que junta casi todo: Scaffold, campo de texto, estado, LazyColumn con key, Checkbox, el truco del Spacer con weight y un diálogo de confirmación.

Lista de tareas

data class Tarea(val id: Int, val titulo: String, val hecha: Boolean = false)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PantallaTareas() {
    val tareas = remember { mutableStateListOf<Tarea>() }
    var texto by rememberSaveable { mutableStateOf("") }
    var aBorrar by remember { mutableStateOf<Tarea?>(null) }

    Scaffold(topBar = { TopAppBar(title = { Text("Tareas (${tareas.count { !it.hecha }})") }) }) { innerPadding ->
        Column(Modifier.padding(innerPadding).padding(16.dp)) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                OutlinedTextField(
                    value = texto,
                    onValueChange = { texto = it },
                    label = { Text("Nueva tarea") },
                    singleLine = true,
                    modifier = Modifier.weight(1f)
                )
                Spacer(Modifier.width(8.dp))
                Button(
                    onClick = {
                        tareas.add(Tarea(id = (tareas.maxOfOrNull { it.id } ?: 0) + 1, titulo = texto.trim()))
                        texto = ""
                    },
                    enabled = texto.isNotBlank()
                ) { Text("Añadir") }
            }

            Spacer(Modifier.height(16.dp))

            LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                items(tareas, key = { it.id }) { tarea ->
                    Card {
                        Row(Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
                            Checkbox(
                                checked = tarea.hecha,
                                onCheckedChange = { marcada ->
                                    val i = tareas.indexOf(tarea)
                                    tareas[i] = tarea.copy(hecha = marcada)
                                }
                            )
                            Text(
                                text = tarea.titulo,
                                textDecoration = if (tarea.hecha) TextDecoration.LineThrough else null
                            )
                            Spacer(Modifier.weight(1f))   // empuja la papelera a la derecha
                            IconButton(onClick = { aBorrar = tarea }) {
                                Icon(Icons.Default.Delete, contentDescription = "Borrar")
                            }
                        }
                    }
                }
            }
        }
    }

    aBorrar?.let { tarea ->
        AlertDialog(
            onDismissRequest = { aBorrar = null },
            title = { Text("¿Borrar «${tarea.titulo}»?") },
            confirmButton = { TextButton(onClick = { tareas.remove(tarea); aBorrar = null }) { Text("Borrar") } },
            dismissButton = { TextButton(onClick = { aBorrar = null }) { Text("Cancelar") } }
        )
    }
}

Siguiente paso: mover tareas a un ViewModel para que no se pierdan al girar la pantalla, y guardarlas con Room o DataStore para que sobrevivan al cerrar la app.

No hay resultados.
Prueba con otra palabra, por ejemplo padding, Lazy o estado.