Este tutorial te mostrará qué es y cómo se utiliza el layout Scaffold En Jetpack Compose con el objetivo de crear interfaces con Material Design a partir de una plantilla que ubica componentes predefinidos sobre un contenedor.
Verás que existe la función componible Scaffold()
para crear patrones de pantallas comunes que se constituyen de elementos como: Top App Bar, Bottom App Bar, Snackbar, Floating Action Button y Navigation Drawer.
Usa la siguiente tabla de contenidos como guía para encontrar el tema el ejemplo que necesites:
Ejemplo Scaffold En Jetpack Compose
Este tutorial usa el ejemplo situado en el paquete examples/Scaffold
del módulo :p7_componentes
que encuentras en mi repositorio de Jetpack Compose:
En él encontrarás al archivo ScaffoldScreent.kt
, origen de todos los ejemplos presentados. No dudes en consultarlo en caso de que una explicación obvie algunas líneas de código.
1. ¿Cómo Usar El Scaffold?
Al principio mencionamos que la función componible Scaffold
representa un layout que implementa la estructura básica del Material Design.
Es decir, internamente agrupa componentes predefinidos, con el fin de acomodarlos en la pantalla mediante los lineamientos del Material Design y asegurándose de que funcionen correctamente en conjunto.
Por esta razón, se le puede considerar como una plantilla que nos evita tener que definir ubicación, márgenes, tamaños y comportamientos de elementos de una pantalla genérica (ejemplo: Bottom App Bar + FAB).
En su forma más sencilla, el Scaffold
se invoca como cualquier layout estándar, recibiendo su contenido en una lambda al final de la línea:
Scaffold {
// Contenido
}
Obviamente nuestra intención es usarlo con más complejidad, debido a la cantidad de componentes que permite incluir su firma:
@Composable
fun Scaffold(
modifier: Modifier! = Modifier,
scaffoldState: ScaffoldState! = rememberScaffoldState(),
topBar: (@Composable () -> Unit)? = {},
bottomBar: (@Composable () -> Unit)? = {},
snackbarHost: (@Composable (SnackbarHostState) -> Unit)? = { SnackbarHost(it) },
floatingActionButton: (@Composable () -> Unit)? = {},
floatingActionButtonPosition: FabPosition! = FabPosition.End,
isFloatingActionButtonDocked: Boolean! = false,
drawerContent: (@Composable @ExtensionFunctionType ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean! = true,
drawerShape: Shape! = MaterialTheme.shapes.large,
drawerElevation: Dp! = DrawerDefaults.Elevation,
drawerBackgroundColor: Color! = MaterialTheme.colors.surface,
drawerContentColor: Color! = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color! = DrawerDefaults.scrimColor,
backgroundColor: Color! = MaterialTheme.colors.background,
contentColor: Color! = contentColorFor(backgroundColor),
content: (@Composable (PaddingValues) -> Unit)?
): Unit
Como ves, es posible añadir: App Bar superior e inferior, Snackbars, FAB y un Navigation Drawer.
Nota que el parámetro scaffoldState
es un objeto con el estado del Scaffold
(información sobre el estado de la pantalla, configuración del drawer y tamaños de los componentes). Para crear un valor inicial de este usa la función rememberScaffoldState()
:
@Composable
fun ScaffoldScreen() {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState
) {
}
}
Dicho esto, estudiemos la manera de usar los demás parámetros para incluir a todos los componentes.
2. Añadir Top App Bar
2.1 Remover ActionBar De La Actividad
En las anteriores versiones de Android la ActionBar
representaba la barra superior en nuestras actividades de forma fija.
Si ya tienes un proyecto existente al cual deseas aplicar Jetpack Compose, es importante remover la ActionBar
usando los temas de Android que terminan en *.NoActionBar
:
<style name="Theme.TuTema" parent="android:Theme.Material.Light.NoActionBar">
<!-- .. -->
</style>
También puedes usar los atributos android:windowActionBar
y android:windowNoTitle
:
<style name="Theme.TuTema.NoActionBar" parent="android:Theme.Material.Light">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
O usar los nuevos estilos Theme.MaterialComponents.*
o Theme.Material3.*
. En este ejemplo usamos el siguiente tema generado automáticamente al crear un proyecto en Android Studio con la plantilla Empty Compose Activity:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.JetpackCompose" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- ... -->
</style>
</resources>
Con esto en mente, pasemos a revisar el elemento de Jetpack Compose que representa la barra superior.
2.2 Componente TopAppBar
El parámetro topBar
representa al slot donde se dibuja la Top App Bar en la pantalla. Aunque puedes pasar tu propia función componible, por lo general usamos el componente TopAppBar
con el fin de crear una barra superior estándar de la librería.
Por ejemplo: Crear la barra que se muestra en la figura 4. Esta se compone de un icono en la parte izquierda para desplegar el drawer, el título y tres botones de acción para búsqueda, favorito y presentación de otras acciones.
La solución consiste en invocar la función TopAppBar()
con los vectores de los iconos mencionados y el título:
@Composable
private fun ExampleTopAppBar() {
TopAppBar(
navigationIcon = {
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = "Abrir menú desplegable")
}
},
title = { Text(text = "Scaffold") },
actions = {
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorito")
}
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Search, contentDescription = "Buscar")
}
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Más")
}
}
)
}
Como ves, usamos los parámetros navigationIcon
, title
y actions
para satisfacer cada slot propuesto.
Luego pasamos la función componible de la barra superior en topBar
:
@Composable
fun ScaffoldScreen() {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
topBar = { ExampleTopAppBar() } // Top App Bar
) {
}
}
Ve al tutorial
TopAppBar
en Jetpack Compose para profundizar sobre su funcionamiento
3. Añadir Bottom App Bar
De forma similar a la Top App Bar, usa el parámetro bottomBar
para mostrar una barra inferior de la pantalla con el componente BottomAppBar
(todo).
Con esto en mente, recreemos la bottom app bar de la figura previa con la siguiente función componible:
@Composable
private fun ExampleBottomAppBar() {
BottomAppBar {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
IconButton(onClick = { /*TODO*/ }) { // (1)
Icon(Icons.Filled.Menu, contentDescription = "Abri menú desplegable")
}
}
Spacer(Modifier.weight(1f, true)) // (2)
IconButton(onClick = { /*TODO*/ }) { // (3)
Icon(imageVector = Icons.Filled.Search, contentDescription = "Buscar")
}
IconButton(onClick = { /*TODO*/ }) { // (4)
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Más")
}
}
}
La función ExampleBottomAppBar()
invoca a BottomAppBar
para añadir a su lambda de contenido:
- El botón para abrir el drawer. Usamos el
CompositionLocalProvider
para establecer su alpha enContentAlpha.high
, ya que el contexto tiene por defectoContentAlpha.medium
- Un espacio entre el botón de menú y los demás
- Un botón de búsqueda
- Un botón para revelar más opciones
Lo siguiente es invocarla en el bottomBar
:
Scaffold(
//...,
bottomBar = { ExampleBottomAppBar() }
) {
}
Ve al tutorial BottomAppBar En Jetpack Compose (todo) para estudiar su uso en profundidad
4. Mostrar Una Snackbar
El parámetro snackbarHost
recibe un objeto del tipo SnackbarHost
, el cual se encarga de mostrar, ocultar y descartar las Snackbars en pantalla.
La firma de Scaffold
toma como argumento un valor por defecto que nos evita su definición. Por lo que puedes mostrar una Snackbar solo ejecutando la función SnackbarHostState.showSnackbar()
.
El SnackbarHostState
controla el orden de aparición en pantalla de la cola de Snackbars. Puedes acceder a este estado con la propiedad snackbarHostState
de tu ScaffoldState
.
Por ejemplo
Mostrar una Snackbar cada vez que el usuario hace clic en los botones de acción de la Top App Bar.
Solución
Cuando un action button de la barra recibe un evento de clic, lo procesamos desde el parámetro onClick
. Para ello modificamos la función ExampleTopAppBar()
para que reciba la función a ejecutar al momento del evento (onActionButtonClick
).
@Composable
private fun ExampleTopAppBar(onActionButtonClick: (String) -> Unit) {
TopAppBar(
navigationIcon = {
IconButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = "Abrir menú desplegable")
}
},
title = { Text(text = "Scaffold") },
actions = {
IconButton(onClick = { onActionButtonClick("Favorito") }) {
Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorito")
}
IconButton(onClick = { onActionButtonClick("Buscar") }) {
Icon(imageVector = Icons.Filled.Search, contentDescription = "Buscar")
}
IconButton(onClick = { onActionButtonClick("Más") }) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Más")
}
}
)
}
En seguida, invocamos a showSnackbar()
desde la lambda que se debe pasar a ExampleTopAppBar
. Como esta función es suspendible, definimos un alcance con rememberCoroutineScope()
:
@Composable
fun ScaffoldScreen() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
ExampleTopAppBar(
onActionButtonClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Clic en '$it'")
}
}
)
},
) {
}
}
Si corres la vista preliminar para ScaffoldScreen
y haces clic en un botón de la barra superior, se mostrará la Snackbar con el nombre de la acción:
Revisa el tutorial Snackbars en Jetpack Compose (todo) para aprender más sobre el componente y personalizar su apariencia desde el parámetro
snackbarHost
.
5. Añadir Floating Action Button
Ahora, si deseas añadir un FAB, entonces pasa como argumento una lambda componible con un componente FloatingActionButton
al parámetro floatingActionButton
del Scaffold
.
Adicionalmente, cuentas con los parámetros floatinActionButtonPosition
y isFloatingActionButtonDocked
. El primero cambia la posición el FAB y el segundo solapa el FAB sobre la Bottom App Bar si es que existe.
Ejemplo
Añadir un Floating Action Button como acción principal en la parte inferior central y superponerlo sobre la barra inferior (ver la figura 7).
Solución
La solución consiste en pasar un componente FloatingActionButton
junto a la posición FabPosition.Center
y el valor de true
para isFloatingActionButtonDocked
.
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Filled.Add, contentDescription = "Crear")
}
},
floatingActionButtonPosition = FabPosition.Center,
isFloatingActionButtonDocked = true
) {
}
Aprende más con el tutorial de Floating Action Button en Jetpack Compose
6. Añadir Navigation Drawer
El Scaffold también te permite incluir el Navigation Drawer (todo) en tu diseño a partir de siete parámetros:
drawerContent
: Define el contenido a dibujar en la hoja lateral que actúa como contenedor del drawerdrawerGesturesEnabled
: Habilita/deshabilita la capacidad del drawer de recibir gestosdrawerShape
: Forma aplicada al contenedor del drawer- drawerElevation: Elevación del contenedor
- drawerBackgroundColor: Color del fondo del contenedor
- drawerContentColor: Color aplicado al contenido de la hoja (texto e iconos)
drawerScrimColor
: Color de la sección oscurecida cuando el drawer está abierto
Ejemplo
Crear un Navigation Drawer con estos cinco ítems de navegación: Bandeja de entrada, Enviados, Archivados, Favoritos y Papelera.
Solución
Con el fin de llevar a cabo el diseño propuesto que se ubicará en la hoja lateral, creamos una función componible que genere una lista de cinco elementos con el componente Column
.
@Composable
private fun DrawerContent() {
val sections = listOf(
"Bandeja de entrada",
"Enviados", "Archivados",
"Favoritos",
"Papelera"
)
Column(Modifier.padding(vertical = 8.dp)) {
sections.forEach { section ->
TextButton(
onClick = { /*TODO*/ },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
val textColor = MaterialTheme.colors.onSurface
Text(
text = section,
style = MaterialTheme.typography.body2.copy(color = textColor)
)
}
}
}
}
}
Luego procesamos el evento de clic en el botón de menú de la app bar con el objetivo de abrir el drawer (la apertura con un swipe a la derecha está disponible por defecto):
@Composable
private fun ExampleTopAppBar(
onMenuButtonClick: () -> Unit, // Acciones a ejecutar
onActionButtonClick: (String) -> Unit
) {
TopAppBar(
navigationIcon = {
IconButton(onClick = onMenuButtonClick) { // Ejecución
Icon(imageVector = Icons.Filled.Menu, contentDescription = "Abrir menú desplegable")
}
},
title = { /*...*/ },
actions = {/*...*/ }
)
}
El siguiente paso es pasar un nuevo parámetro a DrawerContent()
para cerrar el drawer cuando se haga clic en un ítem:
@Composable
private fun DrawerContent(closeDrawer: () -> Unit) { // Cerrar drawer
//...
Column(Modifier.padding(vertical = 8.dp)) {
sections.forEach { section ->
TextButton(
onClick = closeDrawer, // Ejecución
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
) {
/*..*/
}
}
}
}
Y por último invocamos a DrawerContent()
en el parámetro drawerContent
del Scaffold y añadimos las lambdas para la apertura y cierre:
@Composable
fun ScaffoldScreen() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
ExampleTopAppBar(
onMenuButtonClick = {
scope.launch { // Abrir drawer
scaffoldState.drawerState.open()
}
},
onActionButtonClick = {/*...*/ }
)
},
drawerContent = { // Contenido del drawer
DrawerContent { // Cerrar drawer
scope.launch { scaffoldState.drawerState.close() }
}
}
) {
}
}
Ya que necesitamos abrir el drawer al ejecutar onMenuButtonClick
, invocamos la función open()
de la propiedad drawerState
de ScaffoldState
. Y para el cierre tenemos a close()
.
Al correr la previsualización, verás el despliegue del navigation drawer como resultado:
Puedes ver más de este componente en mi tutorial Navigation Drawer en Jetpack Compose (todo).
7. Contenido Del Scaffold
Por último tenemos el contenido que se dibujará en el área principal del Scaffold
con el parámetro content
. Este representa una lambda con un parámetro PaddingValues
, que contiene la cantidad de desplazamiento vertical necesario, a fin de evitar la intersección entre el contenido y las app bars.
Asimismo, si deseas cambiar el color del fondo o el contenido en el Scaffold, usa los parámetros backgroundColor
y contentColor
.
Ejemplo
Añadamos como ejemplo un contenido una opción que permita seleccionar entre Top o Bottom Bar con RadioButtons y el cambio de color del fondo del Scaffold con el clic en un botón.
Solución
La jerarquía del diseño la resolvemos al:
- Recibir como parámetro el padding adicional del Scaffold, el nombre de la app bar seleccionada, la función para seleccionar la app bar y la función a ejecutar cuando se hace clic en el botón
- Crear un layout
Column
para presentar verticalmente ambas opciones - Añadir una
Row
para dosRadioButtons
- Añadir una elemento
Button
El código equivalente a las tareas anteriores es:
@Composable
fun ScaffoldContent( // (1)
padding: PaddingValues,
appBarSelected: String,
selectAppBar: (String) -> Unit,
onButtonClick: () -> Unit
) {
Column( // (2)
modifier = Modifier.padding(top = 16.dp, bottom = padding.calculateBottomPadding()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row { // (3)
LabelledRadioButton(
label = appBarOptions[0],
selected = appBarSelected == appBarOptions[0],
onClick = { selectAppBar(appBarOptions[0]) })
LabelledRadioButton(
label = appBarOptions[1],
selected = appBarSelected == appBarOptions[1],
onClick = { selectAppBar(appBarOptions[1]) }
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onButtonClick) { // (4)
Text(text = "Cambiar backgroundColor")
}
}
}
A continuación definimos los estados para la selección de la app bar y el color del fondo del Scaffold
:
// Opciones
private val appBarOptions = listOf("TopAppBar", "BottomAppBar")
@Composable
fun ScaffoldScreen() {
//...
// Estados
val (appBarSelected, selectAppBar) = remember { mutableStateOf(appBarOptions.first()) }
var scaffoldBackgroundColor by remember { mutableStateOf(Color.White) }
Scaffold(
scaffoldState = scaffoldState,
topBar = {
if (appBarSelected == appBarOptions[0]) // Condicionar top bar
ExampleTopAppBar(/*...*/)
},
bottomBar = {
if (appBarSelected == appBarOptions[1]) // Condicionar bottom bar
ExampleBottomAppBar()
},
backgroundColor = scaffoldBackgroundColor // Color de fondo
) { padding ->
ScaffoldContent(
padding = padding,
appBarSelected = appBarSelected,
selectAppBar = selectAppBar,
onButtonClick = { // Generar color aleatorio
scaffoldBackgroundColor = Color(Random.nextLong(0xFFFFFFFF))
}
)
}
}
De esta forma, al ejecutar verás la modificación del fondo y el intercambio entre la Top App Bar y la Bottom App Bar.
¿Qué Sigue?
Este tutorial sobre el Scaffold en Jetpack Compose te permitió explorar el uso del componente y sus parámetros asociados. Viste como facilita la creación de pantallas comunes al momento de usar Material Design.
Se crearon varios ejemplos con el Scaffold para evidenciar su funcionamiento, pero por razones de simplicidad, se limitó la explicación de los componentes que hacen parte del layout. Para expandir el conocimiento sobre ellos ve a los siguientes tutoriales:
- Top App Bar
- Bottom App Bar
- Snackbar
- Floating Action Button
- Navigation Drawer