Compose: Scaffold, slots y tema
Al terminar: Montar la pantalla del Marcador con Scaffold, slots y el tema de la app, y verla en el emulador.
Con Row, Column y Box colocas piezas. Falta el esqueleto que las sostiene. Al terminar esta lección, el Marcador tendrá una pantalla de verdad: una barra superior con el título, abajo un botón «Pruebas de Android» y, en el centro, la tarjeta «Rayo FC 2 - 1 Toros», los dos recuadros del orden de los modifier y el escudo con su insignia. Todavía no se puede tocar nada, salvo ese botón, que te lleva a la pantalla de pruebas de las lecciones de Android: la pantalla es estática. Los botones que cambian el marcador llegan en Estado en Compose.
Importa porque todas las pantallas Material de la app se montan sobre un Scaffold. Temas: Scaffold y slots, la trampa del @OptIn, el tema de Material 3 y conectar la pantalla nueva con MainActivity.
Scaffold y slots
Un slot es un parámetro que recibe un composable: en vez de titulo: String, topBar: @Composable () -> Unit, y decides tú qué se dibuja dentro. Scaffold es el esqueleto de una pantalla Material y trae varios slots. Regla de las funciones con slots: modifier va el primero de los opcionales y el slot de la lambda final (content) va detrás, para escribirlo fuera de los paréntesis (la trailing lambda).
Un Scaffold con dos slots y una Card (el panel con fondo y esquinas redondeadas) que lleva dentro la ficha de la lección anterior:
@OptIn(ExperimentalMaterial3Api::class) // acepta usar la barra experimental
@Composable
fun PantallaPlantilla(onGuardar: () -> Unit, modifier: Modifier = Modifier) { // el evento llega como una función
Scaffold(
modifier = modifier, // el modifier de fuera se lo pasamos al Scaffold
topBar = { CenterAlignedTopAppBar(title = { Text("Plantilla") }) }, // slot de arriba: recibe un composable
bottomBar = { Button(onClick = onGuardar) { Text("Guardar") } }, // slot de abajo
) { paddingValues -> // lo que ocupan las barras
Card(Modifier.padding(paddingValues).padding(16.dp)) { // primero paddingValues, luego tu margen
FichaDeJugador("Ana", dorsal = 9)
}
}
}Lo que se ve: arriba, la barra con «Plantilla» centrado. Abajo, a la izquierda, el botón «Guardar». En medio, una tarjeta con la ficha de Ana: el círculo con el 9, la «C» en su esquina, «Ana» y «Delantera».
En la Card, el padding(paddingValues) va antes del padding(16.dp), pero el orden no cambia nada visible: dos padding seguidos se suman (el orden solo importa cuando hay un background entre ellos, como viste en la lección anterior). Lo que no puede faltar es paddingValues. Para un botón redondo que flote sobre la esquina inferior derecha, el slot es floatingActionButton.
Y si quieres que tu propia función tenga un slot, declaras un parámetro de tipo @Composable () -> Unit y lo dibujas llamándolo como a una función: dentro de tu función escribes contenido() y ahí aparece lo que te pasaron entre llaves.
El tema: Material 3, MaterialTheme y la carpeta ui.theme
Material 3 es el sistema de diseño de Google: un conjunto de componentes (Button, Card, Scaffold…) y un tema, con colores, tipografía y formas, que esos componentes usan solos. Un tema es el lugar único donde se decide el aspecto general, para no repetir un color en cada pantalla.
En Compose, el tema se pone con el composable MaterialTheme { … }: todo lo que lleva dentro puede pedir MaterialTheme.colorScheme.primary (un color) o MaterialTheme.typography.titleMedium (un estilo de texto). Es lo que ya tiene tu MainActivity: setContent { MaterialTheme { … } }. Un mini ejemplo del mismo Text pidiendo el color primario, dentro y fuera de un tema propio:
// Sin tema propio: usa los colores por defecto de Material (la paleta clara).
Text("Hola", color = MaterialTheme.colorScheme.primary)
// Dentro de un MaterialTheme con la paleta oscura: el mismo código pide «el primario»
// y recibe otro color (un violeta más pálido). El Text no cambia; cambia el tema.
MaterialTheme(colorScheme = darkColorScheme()) {
Text("Hola", color = MaterialTheme.colorScheme.primary)
}Funciona porque MaterialTheme deja sus colores disponibles para todo lo que hay dentro, sin pasarlos como parámetro de uno en uno (Compose lo llama CompositionLocal).
Y la carpeta theme (dentro de ui: la vista Android la muestra como ui.theme en una sola línea porque ui solo tiene esa hija) en app → kotlin+java → com.sebastiannarvaez.marcador: la plantilla de Android Studio la creó con tres ficheros, Color.kt (la paleta), Type.kt (la tipografía) y Theme.kt. En Theme.kt vive MarcadorTheme, un composable propio que llama a MaterialTheme con esa paleta, con un modo claro y uno oscuro, y con colores dinámicos (en Android 12 y posteriores, toma los colores del fondo de pantalla del usuario). Es la forma habitual de organizar el tema de una app real.
En esta guía todavía no lo usas: MainActivity pide MaterialTheme directamente, con sus colores por defecto, y ui.theme queda ahí sin usar. No lo borres ni lo toques; así las pantallas se ven igual en cualquier móvil y las capturas de la guía coinciden con las tuyas. Si más adelante cambias MaterialTheme { … } por MarcadorTheme { … }, el resto del código no cambia: eso es lo que da un tema.
Reto · Ejercicio: la pantalla de una receta
Crea en la carpeta ejercicios de tu proyecto (la de La rotación y la muerte del proceso) una subcarpeta nueva, pantalla_de_receta (clic derecho sobre com.sebastiannarvaez.marcador → New → Package → escribe .ejercicios.pantalla_de_receta, con el punto delante), y dentro un fichero de Kotlin PantallaDeReceta (clic derecho sobre la subcarpeta → New → Kotlin Class/File → File).
Escribe en él dos composables.
TarjetaConTitulo: recibe un título, un modificador opcional y un slot para el contenido, que va el último para escribirlo fuera de los paréntesis. Dibuja una tarjeta a todo lo ancho con, dentro y con 16 dp de margen, el título en el color primario del tema y, debajo, el contenido que le pasen.PantallaDeReceta: recibe qué hacer al pulsar «Empezar a cocinar» y un modificador opcional. Es unScaffoldcon la barra superior centrada «Tortilla de patatas» y, abajo, el botón «Empezar a cocinar» con 16 dp de margen. En el contenido, aplicando lo que ocupan las barras y 16 dp más, van dosTarjetaConTituloseparadas 16 dp: «Ingredientes» (4 huevos, 3 patatas, 1 cebolla) y «Tiempo» (30 minutos).
Escribe dos vistas previas: una con el tema tal cual y otra con la paleta oscura, y ábrelas en Split (si pide compilar, Build & Refresh). En la primera debes ver la barra con «Tortilla de patatas» centrado, dos tarjetas con su título en violeta y sus líneas, y abajo a la izquierda el botón. En la segunda, la misma pantalla sobre fondo oscuro, con otros colores y sin cambiar una línea de PantallaDeReceta: eso es lo que da un tema.
Ver solución
package com.sebastiannarvaez.marcador.ejercicios.pantalla_de_receta
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
// Un composable propio con un slot: el título lo pone él, el contenido lo decide quien lo llama.
@Composable
fun TarjetaConTitulo(titulo: String, modifier: Modifier = Modifier, contenido: @Composable () -> Unit) {
Card(modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(titulo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
contenido()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PantallaDeReceta(onEmpezar: () -> Unit, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
topBar = { CenterAlignedTopAppBar(title = { Text("Tortilla de patatas") }) },
bottomBar = { Button(onClick = onEmpezar, Modifier.padding(16.dp)) { Text("Empezar a cocinar") } },
) { paddingValues ->
Column(
Modifier.padding(paddingValues).padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TarjetaConTitulo("Ingredientes") {
Text("4 huevos")
Text("3 patatas")
Text("1 cebolla")
}
TarjetaConTitulo("Tiempo") {
Text("30 minutos")
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PantallaDeRecetaPreview() {
MaterialTheme { PantallaDeReceta(onEmpezar = {}) }
}
@Preview(showBackground = true)
@Composable
private fun PantallaDeRecetaOscuraPreview() {
MaterialTheme(colorScheme = darkColorScheme()) { PantallaDeReceta(onEmpezar = {}) }
}Paso · la pantalla del marcador, con su Scaffold
Objetivo. Añadir PantallaMarcador a PantallaMarcador.kt, el esqueleto que usa las tres piezas de la lección anterior. Al terminar verás en la vista previa: la barra «Marcador», la tarjeta, los recuadros A y B, el círculo «RF» y, abajo, el botón «Pruebas de Android».
Piezas nuevas de este paso:
Scaffold(topBar = …, bottomBar = …) { paddingValues -> … }: el esqueleto; el contenido va en la lambda final.CenterAlignedTopAppBar(title = { Text("…") }): barra superior con el título centrado (pide@OptIn).Arrangement.Centeren unRow: centra los hijos a lo largo del eje principal, aquí el horizontal.
PantallaMarcador es el esqueleto de la pantalla: recibe qué hacer al pulsar el botón de abajo y, opcionalmente, un modificador. Pone la barra superior «Marcador» con el título centrado, abajo un botón centrado «Pruebas de Android» y, en el centro, en columna y separados 24 dp, los tres composables de antes con el partido de ejemplo.
Hace falta importar cuatro nombres más: Button, CenterAlignedTopAppBar, ExperimentalMaterial3Api y Scaffold (Option+Enter sobre cada uno en rojo). Con ellos, el fichero tiene 28 imports. PantallaMarcador va encima de TarjetaDePartido, justo debajo de los imports.
Intenta escribirlo tú: PantallaMarcador(onVerDemos: () -> Unit, modifier: Modifier = Modifier) (el parámetro se llama onVerDemos y se queda con ese nombre: lo usan las lecciones siguientes). Después, borra PiezasDelMarcadorPreview y escribe en su lugar PantallaMarcadorPreview, que dibuja la pantalla con fondo blanco y un botón que no hace nada. Lo que debes ver en la vista previa: la barra «Marcador», la tarjeta Rayo FC 2 - 1 Toros, los recuadros A y B, el círculo «RF» con el «2» rojo, y abajo el botón «Pruebas de Android». Si te atascas, abre el bloque.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Los cuatro import que faltan, junto a los demás:
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ScaffoldLa pantalla con su Scaffold, encima de TarjetaDePartido:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PantallaMarcador(onVerDemos: () -> Unit, modifier: Modifier = Modifier) {
val partido = Ejemplo.rayoContraToros
Scaffold(
modifier = modifier,
topBar = { CenterAlignedTopAppBar(title = { Text("Marcador") }) },
bottomBar = {
Row(Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.Center) {
Button(onClick = onVerDemos) { Text("Pruebas de Android") }
}
},
) { paddingValues ->
// SIEMPRE se usa paddingValues (si no, el contenido queda tapado por las barras).
Column(
Modifier.fillMaxSize().padding(paddingValues).padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
TarjetaDePartido(partido, partido.marcador())
OrdenDeLosModifiers()
InsigniaSobreEscudo()
}
}
}La vista previa de la pantalla entera, en lugar de PiezasDelMarcadorPreview:
@Preview(showBackground = true)
@Composable
private fun PantallaMarcadorPreview() {
MaterialTheme { PantallaMarcador(onVerDemos = {}) }
}Paso · MainActivity muestra la pantalla nueva
Hasta ahora, setContent pintaba directamente un Column con los botones de las lecciones de Android. Ahora pinta PantallaMarcador, y ese Column se mueve a una función propia, PantallaDePruebas (la pantalla de pruebas), que solo se muestra si el usuario pulsa el botón. Una propiedad, verPruebas, decide cuál de las dos se ve.
Piezas nuevas de este paso:
if (…) A else B: unifnormal que elige qué función se llama,PantallaDePruebasoPantallaMarcador, según el valor deverPruebas.private var verPruebas by mutableStateOf(false): una propiedad que Compose observa; cambiarla repinta la pantalla (se explica en Estado en Compose). Un composable también puede ser un método de la Activity, comoPantallaDePruebas.
La misma idea con otras dos pantallas inventadas, Inicio y Ajustes:
private var verAjustes by mutableStateOf(false) // propiedad observada: al cambiar, se repinta
// Dentro de setContent { MaterialTheme { … } }:
if (verAjustes) Ajustes() else Inicio(onAbrirAjustes = { verAjustes = true }) // solo se dibuja una de las dos
@Composable
private fun Ajustes() { // un composable puede ser un método de la Activity
Column {
Button(onClick = { verAjustes = false }) { Text("Volver") } // cambiar la propiedad devuelve a Inicio
Text("Ajustes")
}
}Abre app → kotlin+java → com.sebastiannarvaez.marcador → MainActivity.kt. El Column de pruebas usaba una variable, tabla, que onCreate calcula justo antes de setContent; al sacarlo a otra función, esa variable no le llega sola, y por eso PantallaDePruebas la recibe como parámetro y el if se la pasa. Tres movimientos, en este orden (el Column de pruebas solo cambia de sitio; no se modifica):
- Dentro de
setContent { MaterialTheme { … } }, selecciona elColumn(…) { … }entero, desde su primera línea hasta su llave de cierre, y córtalo (Cmd+X). En su lugar escribe la líneaif (…)del primer bloque de abajo. Las llaves deMaterialTheme, desetContenty deonCreatese quedan donde están. - Justo después de la llave que cierra
onCreate, escribeverPruebasy la cabecera dePantallaDePruebascon su llave de cierre. Dentro de la función, pega elColumnque cortaste (Cmd+V). - Como primer hijo de ese
Column, antes deText("Marcador"), el botón para volver.
Los tres import nuevos los propone Option+Enter sobre cada nombre en rojo (Composable, FilaDePosicion, PantallaMarcador). Los de getValue, setValue y mutableStateOf, que necesita la propiedad verPruebas, ya los tiene MainActivity desde las lecciones de Android (el permiso de las notificaciones usa la misma forma); si alguno saliera en rojo, Option+Enter lo añade igual. Ningún bloque de abajo es el fichero entero: cada uno es solo lo que añades o sustituyes. En particular, el bloque titulado «MainActivity.kt: la propiedad y la función» no es el fichero entero, solo lo que añades justo después de onCreate.
Intenta escribirlo tú: una propiedad observada, verPruebas, que empieza en falso; al crear la pantalla, si es verdadera se dibuja la pantalla de pruebas, PantallaDePruebas, que recibe la tabla, y si no, PantallaMarcador, cuyo botón la pone a verdadero. En la pantalla de pruebas, el primer elemento es un botón «Volver a la pantalla del marcador» que la pone a falso. Lo que debes ver: al arrancar, la pantalla del marcador; «Pruebas de Android» abre la de pruebas y su botón de arriba te devuelve.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Los import nuevos, junto a los demás:
import androidx.compose.runtime.Composable
import com.sebastiannarvaez.marcador.torneo.FilaDePosicion
import com.sebastiannarvaez.marcador.feature.marcador.ui.PantallaMarcadorLa línea que sustituye al Column dentro de setContent { MaterialTheme { … } }:
if (verPruebas) PantallaDePruebas(tabla) else PantallaMarcador(onVerDemos = { verPruebas = true })Lo que va justo después de la llave que cierra onCreate, con el Column pegado dentro de la función:
private var verPruebas by mutableStateOf(false)
@Composable
private fun PantallaDePruebas(tabla: List<FilaDePosicion>) {
// aquí, el Column que cortaste, con todos sus botones
}Y el botón para volver, como primer hijo del Column:
Button(onClick = { verPruebas = false }) { Text("Volver a la pantalla del marcador") }Si MainActivity no compila, cuenta las llaves: onCreate termina justo antes de verPruebas, y PantallaDePruebas tiene su propia llave de cierre después del Column.
Paso · mira la vista previa y ejecuta
1 · La vista previa. Abre PantallaMarcador.kt y pulsa Split (como en la lección anterior; si pide compilar, Build & Refresh). Debes ver la pantalla entera con un fondo blanco: la barra, la tarjeta, los recuadros A y B, el escudo «RF» y el botón.
2 · Ejecuta. Con el emulador arrancado, la configuración app elegida arriba (como en La Activity) y pulsa Run (el triángulo verde). La variante es la activa de Build Variants (Gradle: flavors y variantes); con gratisDebug o proDebug se ve igual. Verás la barra «Marcador», la tarjeta Rayo FC 2 - 1 Toros, el recuadro A pegado al texto y el B con aire alrededor, y el círculo «RF» con el «2» rojo en la esquina. Pulsa «Pruebas de Android»: se abre la pantalla de pruebas de las lecciones de Android, y su botón de arriba te devuelve.
Las barras del sistema. En los móviles recientes la app se dibuja por debajo de la barra de estado (arriba: hora, batería) y de la barra de gestos o de navegación (abajo). Por eso a veces hay que reservar ese hueco a mano. Si el botón «Pruebas de Android» queda parcialmente tapado por la de abajo, añade .navigationBarsPadding() justo antes de .padding(16.dp) en el Row de bottomBar: reserva el alto de esa barra.
Punto de control
- En el modo Split, la vista previa de
PantallaMarcador.ktmuestra la barra «Marcador», la tarjeta, los recuadros A y B y el círculo «RF» con su «2» rojo. - En el emulador (Run, con la configuración
app) se ve esa misma pantalla, el recuadro A pegado al texto y el B con aire alrededor. - El botón «Pruebas de Android» abre la pantalla de pruebas de las lecciones de Android (con sus botones), y el botón de arriba te devuelve a la pantalla del marcador.
- Los tests de
:torneosiguen en 137: esta lección no los toca.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Escribes un
Scaffoldy el contenido se dibuja tapado por la barra superior. ¿Qué falta?2.¿Qué es un slot en Compose?
3.¿Para qué sirve
@OptIn(ExperimentalMaterial3Api::class)encima de una función?4.¿Qué hace
MaterialTheme { … }alrededor de tus pantallas?5.El mismo
Text("Hola", color = MaterialTheme.colorScheme.primary)se ve de otro color dentro deMaterialTheme(colorScheme = darkColorScheme()) { … }. ¿Por qué?