TopAppBar En Jetpack Compose

En este tutorial aprenderás cómo usar la TopAppBar en Jetpack Compose para proveer una barra superior en tus aplicaciones Android, que permita mostrar información y acciones relacionadas a la pantalla que estés diseñando.

Top App Bar
Figura 1. Top App Bar

Si ya viste mi tutorial sobre el componente Scaffold, sabrás que la función componible que estudiaremos es TopAppBar(). Esta posee una estructura de slots, donde podemos ubicar un icono de navegación, el título y varios botones de acción.

La idea es explorar los parámetros que nos brinda para crear app bars sujetas a las especificaciones del Material Design. Siéntete libre de navegar por el contenido con la siguiente tabla:

Ejemplo De Top App Bar En Jetpack Compose

Puedes encontrar todos los ejemplos que aquí son descritos en mi repositorio de GitHub:

Navega hacia el módulo :p7_componentes y ubica el paquete TopAppBar. En el se encuentra el archivo kotlin TopAppBarScreen.kt, el cual integra a todos los ejemplos que verás.

Ejemplo de TopAppBar en Jetpack Compose

¿Cómo Añadir La Top App Bar?

Top App Bar con título
Figura 2. Top App Bar con título

La Top App Bar en Jetpack Compose es representada por la función TopAppBar(). En su forma más sencilla puedes presentarla con un título de la siguiente manera:

TopAppBar(
    title = {
        Text("Develou")
    }
)

Si la incrustas en el elemento Scaffold tendrás más comodidad a la hora de construir layouts complejos.

@Composable
fun TopAppBarScreen() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text("Develou")
                }
            )
        }
    ) {
        Box(Modifier.fillMaxSize()) {
            Text(
                text = "Contenido",
                modifier = Modifier.align(Alignment.Center)
            )
        }
    }
}

Recuerda que el parámetro topBar recibe a la TopAppBar y la lambda del final al contenido principal de la pantalla.

Top App Bar con contenido
Figura 3. Top App Bar con contenido

Sin embargo, aunque es recomendable usar el Scaffold, no es obligatorio. Puedes usarla con cualquier layout estándar; por ejemplo, con una Column:

@Composable
fun TopAppBarScreen() {
    Column {

        TopAppBar(
            title = {
                Text("Develou")
            }
        )
        Box(Modifier.fillMaxSize()) {
            Text(
                text = "Contenido",
                modifier = Modifier.align(Alignment.Center)
            )
        }
    }
}

1. Crear Una TopAppBar

Habiendo presentado la anterior información, veamos las dos sobrecargas que la librería nos brinda para crear barras superiores.

1.1 Sobrecarga Fija

Esta versión permite crear Top App Bars con tres espacios predefinidos para icono de navegación(navigationIcon), título (title) y botones de acción (actions). Por esta razón, sus parámetros están restringidos de la siguiente forma:

@Composable
fun TopAppBar(
    title: (@Composable () -> Unit)?,
    modifier: Modifier! = Modifier,
    navigationIcon: (@Composable () -> Unit)? = null,
    actions: (@Composable @ExtensionFunctionType RowScope.() -> Unit)? = {},
    backgroundColor: Color! = MaterialTheme.colors.primarySurface,
    contentColor: Color! = contentColorFor(backgroundColor),
    elevation: Dp! = AppBarDefaults.TopAppBarElevation
): Unit

Adicionalmente, posee parámetros para los colores de fondo (backgroundColor) y contenido (contentColor); y para especificar la elevación (elevation).

Ejemplo: Top App Bar Común

TopAppBar con layout fijo
Figura 4. TopAppBar con slots fijos

Materialicemos la app bar de la figura 4, la cual tiene:

  • Icono de navegación para retorno
  • El texto «Top App Bar» en el título
  • Tres action buttons para marcar como favorito, compartir y el menú de overflow.

¿Cómo solucionarlo?

Sencillo. Escribimos los nombres de los parámetros y pasamos lambdas cuyo cuerpo sea del contenido apropiado. Es decir, para el título usamos a Text() y para los botones IconButton():

@Composable
internal fun DetailTopAppBar() {
    TopAppBar(
        navigationIcon = {
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Ir hacia arriba")
            }
        },
        title = { Text(text = "Top App Bar") },
        actions = {
            IconButton(onClick = { /*TODO*/ }) {
                Icon(
                    painter = painterResource(id = R.drawable.ic_bookmark),
                    contentDescription = "Leer después"
                )
            }
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Filled.Share, contentDescription = "Compartir")
            }

            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Ver más")
            }
        }
    )
}

Recuerda que Jetpack Compose cuenta con el objeto Icons con una gran variedad de vectores para iconos comunes.

En nuestro caso, el icono de marcador no está disponible, por esta razón lo añadimos como recurso drawable y lo leemos con painterResource().

Más vectores de iconos: Puedes acceder a todos los iconos de Material Design si incluyes la dependencia androidx.compose.material:material-icons-extended, pero debido a su gran tamaño, Google recomienda usar vectores desde recursos ó copiar y pegar directamente la definición Kotlin del vector a tu proyecto.

1.2 Sobrecarga Flexible

Usa esta sobrecarga si deseas personalizar el layout de todo el contenido en la Top App Bar. Debido a esta libertad, los parámetros usados en el ejemplo anterior son reemplazados por una lambda de contenido (content) y el padding del mismo (contentPadding):

@Composable
fun TopAppBar(
    modifier: Modifier! = Modifier,
    backgroundColor: Color! = MaterialTheme.colors.primarySurface,
    contentColor: Color! = contentColorFor(backgroundColor),
    elevation: Dp! = AppBarDefaults.TopAppBarElevation,
    contentPadding: PaddingValues! = AppBarDefaults.ContentPadding,
    content: (@Composable @ExtensionFunctionType RowScope.() -> Unit)?
): Unit

Ejemplo: Centrar Título En Top App Bar

TopAppBar con título centrado
Figura 5. TopAppBar con título centrado

Creemos una app bar cuyo título esté centrado, añadamos un ítem de menú en la parte izquierda y un action button como se muestra en la figura anterior.

¿La solución?

Partamos del hecho de que el recibidor en content es de tipo RowScope, por lo que los elementos que pasemos están dispuestos a convivir de forma horizontal.

Por esta razón, usaremos el modificador weight() sobre el componente Text() que representa al título, a fin de ocupar el espacio restante entre ambos extremos. Y además, aplicamos el valor de TextAlign.Center al parámetro textAlign.

@Composable
internal fun CenterAlignedTopAppBar() {
    TopAppBar {
        CompositionLocalProvider(
            LocalContentAlpha provides ContentAlpha.high
        ) {
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Filled.Menu, contentDescription = "Abrir menú")
            }
        }

        CompositionLocalProvider(
            LocalContentAlpha provides ContentAlpha.high,
            LocalTextStyle provides MaterialTheme.typography.h6
        ) {
            Text(
                text = "Título Centrado",
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
            )
        }

        IconButton(onClick = { /*TODO*/ }) {
            Icon(imageVector = Icons.Filled.Settings, contentDescription = "Ajustes")
        }
    }
}

Ten en cuenta que el icono de navegación y el título deben tener un énfasis alto en su componente alpha. Por este motivo el código anterior los recubre con CompositionLocalProvider, ya que aplicamos ContentAlpha.high.

Y en el caso del texto, también le aplicamos el estilo h6 con LocalTextStyle.

Por otro lado, si deseas mayor flexibilidad en el contenido, entonces crea tu propia app bar a partir de layouts como Box, para orientar y arreglar los componentes según tus necesidades.

Título centrado en M3: En esta librería encuentras al componente CenterAlignedTopAppBar, el cual cumple con el centrado de título de forma preconstruida.


2. Añadir Action Items

Las acciones principales de tu App se pueden representar como action items (o action buttons) en la barra superior. Vimos que se ubican en la parte derecha del contenido en la TopAppBar.

Según su frecuencia de uso, el botón más usado va más a la izquierda y por ende los menos usados hacia la derecha. Y si la cantidad de elementos es grande, los botones de acción que no quepan se incluyen en el overflow menu.

2.1 Manejar Eventos De Click En Ítems

El procesamiento de clicks requiere pasar lambdas con las acciones a ejecutar en los IconButtons. Recuerda que este componente te ofrece el parámetro onClick para dicho objetivo.

Ejemplo: Click En Action Items

TopAppBar con búsqueda y filtro
Figura 6. TopAppBar con búsqueda y filtro

Tomemos como caso de estudio una app bar típica de una vista de lista. Esta contendrá tres action items: Buscar, Filtrar y el icono del overflow menu (figura 6).

¿Cómo procedemos?

En primer lugar, creemos una nueva función componible llamada ListTopAppBar() y construyamos el diseño:

@Composable
internal fun ListTopAppBar() {
    TopAppBar(
        title = { Text(text = "Action Items") },
        navigationIcon = {
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Filled.Menu, contentDescription = "Abrir menú")
            }
        },
        actions = {

            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Filled.Search, contentDescription = "Buscar")
            }

            IconButton(onClick = { /*TODO*/ }) {
                Icon(
                    painter = painterResource(id = R.drawable.ic_filter_list),
                    contentDescription = "Filtrar"
                )
            }

            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Ver más")
            }
        }
    )
}

Ahora, procedamos con el enunciado propuesto al inicio de la sección: añadamos parámetros con las lambdas a ejecutar que serán pasadas a cada ítem de acción.

Usemos como nombre una oración en inglés del tipo on<Action>Click. Por ejemplo, para la búsqueda sería onSearchClick (al clickear buscar).

@Composable
internal fun ListTopAppBar(
    onSearchClick: () -> Unit, // (1.1)
    onFilterClick: () -> Unit // (1.2)
) {
    TopAppBar(
        title = { /*...*/ },
        navigationIcon = {/*...*/ },
        actions = {

            IconButton(onClick = onSearchClick) { // (2.1)
                /*...*/
            }

            IconButton(onClick = onFilterClick) { // (2.2)
                /*...*/
            }

            /*...*/
        }
    )
}

Por último, haz un llamado de la función y pasa las acciones en cada parámetro. En mi caso, por simplicidad del tutorial, mostraré una Snackbar(todo) con el nombre de la acción.

@Composable
private fun GeneralizedActionItems(showSnackbar: (String) -> Unit) {
    ListTopAppBar(
        openDrawer = {},
        actionItems = listOf(
            ActionItem(
                "Buscar",
                Icons.Filled.Search,
                action = { showSnackbar("Buscar") }
            ),
            ActionItem(
                "Filtrar",
                ImageVector.vectorResource(id = R.drawable.ic_filter_list),
                action = { showSnackbar("Filtrar") }
            )
        )
    )
}

Al correr la vista previa y clickear en uno de los dos botones de acción se muestra la Snackbar:

Manejar eventos de click en action buttons

2.2 Generalizar Action Items

En el momento en que nuestra cantidad de action items comience a incrementar, la firma de nuestra Top App Bar comenzará a crecer con la cantidad de acciones que debemos pasar.

Para evitar esa situación, crearemos una clase de datos como abstracción para los ítems. La idea es representar sus propiedades comunes: nombre, icono y acción.

data class ActionItem(
    val name: String,
    val icon: ImageVector,
    val action: () -> Unit
)

De esta manera podemos reutilizar la app bar en varios lugares y así personalizar los botones que se incluyen. Por ejemplo, la barra anterior podemos actualizarla a la siguiente implementación:

@Composable
internal fun ListTopAppBar(
    openDrawer: () -> Unit,
    actionItems: List<ActionItem> // (1) Añadir parámetro
) {
    TopAppBar(
        title = { /*..*/ },
        navigationIcon = {
            IconButton(onClick = openDrawer) {
                /*..*/
            }
        },
        actions = {

            actionItems.forEach { // (2) Crear action items desde lista
                IconButton(onClick = it.action) {
                    Icon(imageVector = it.icon, contentDescription = it.name)
                }
            }

            /*..*/
        }
    )
}

Observa que en (1) pasamos una lista de acciones como parámetro y en (2) invocamos a forEach() para crear cada botón.

No obstante, debemos considerar el botón de overflow para mejorar nuestra solución.


3.3 Overflow Menu

Como ya vimos en mi tutorial Menus en Jetpack Compose, el componente DropdownMenu representa un menú desplegable con varias opciones del tipo DropdownMenuItem.

Así que este será el elemento con que crearemos el overflow menu de la TopAppBar.

Ejemplo: Desplegar Overflow Menu

Overflow menu en TopAppBar
Figura 7. Overflow menu en TopAppBar

Agreguemos una app bar superior con cinco acciones en su overflow menu: Refrescar, Ajustes, Enviar sugerencias, Ayuda y Cerrar sesión.

¿Aspectos a tener en cuenta?

Necesitamos el nombre del ítem para mostrarlo como opción del menú y su orden de importancia.

Estos requisitos hacen que nuestra clase ActionItem expanda sus propiedades:

data class ActionItem(
    val name: String,
    val icon: ImageVector? = null,
    val action: () -> Unit,
    val order: Int
)

Luego, clasificamos aquellos ítems que serán mostrados como iconos y aquellos que irán al menú:

@Composable
internal fun ListTopAppBar(
    openDrawer: () -> Unit,
    actionItems: List<ActionItem>
) {
    TopAppBar(
        title = { /*...*/ },
        navigationIcon = {/*...*/},
        actions = {
            val (icons, options) = actionItems.partition { it.icon != null } // (1)

            icons.forEach { // (2)
                IconButton(onClick = it.action) {
                    Icon(imageVector = it.icon!!, contentDescription = it.name)
                }
            }

            val (isExpanded, setExpanded) = remember { mutableStateOf(false) } // (3)

            OverflowMenuAction(isExpanded, setExpanded, options) // (4)
        }
    )
}

Del código anterior se destaca que:

  1. Usamos la función partition() para particionar la lista de acciones en dos componentes: icons y options
  2. Iteramos los iconos para mostrarlos como IconButtons
  3. Declaramos el estado para realizar seguimiento a la visibilidad del menú
  4. Creamos el menú con los componentes del estado y las opciones a crear como ítems

La función OverflowMenuAction() contiene el IconButton para accionar su despliegue y la creación de un DropdownMenu:

@Composable
private fun OverflowMenuAction(
    expanded: Boolean,
    setExpanded: (Boolean) -> Unit,
    options: List<ActionItem>
) {
    IconButton(onClick = { setExpanded(true) }) {
        Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Ver más")

        DropdownMenu(
            expanded = expanded,
            onDismissRequest = { setExpanded(false) },
            offset = DpOffset(x = 0.dp, y = 4.dp)
        ) {
            options.forEach { option ->
                DropdownMenuItem(
                    onClick = {
                        option.action()
                        setExpanded(false)
                    }
                ) {
                    Text(text = option.name)
                }
            }
        }
    }
}

Terminamos invocando la top bar con la lista ActionItem:

@Composable
private fun OverflowMenuExample(showSnackbar: (String) -> Unit) {

    val actionItems = listOf(
        ActionItem(
            "Buscar",
            Icons.Filled.Search,
            action = { showSnackbar("Buscar") },
            order = 1
        ),
        ActionItem(
            "Filtrar",
            ImageVector.vectorResource(id = R.drawable.ic_filter_list),
            action = { showSnackbar("Filtrar") },
            order = 2
        ),
        ActionItem(
            "Refrescar",
            action = { showSnackbar("Refrescar") },
            order = 3
        ),
        ActionItem(
            "Ajustes",
            action = { showSnackbar("Ajustes") },
            order = 4
        ),
        ActionItem(
            "Enviar sugerencias",
            action = { showSnackbar("Enviar sugerencias") },
            order = 5
        ),
        ActionItem(
            "Ayuda",
            action = { showSnackbar("Ayuda") },
            order = 6
        ),
        ActionItem(
            "Cerrar sesión",
            action = { showSnackbar("Cerrar sesión") },
            order = 7
        )
    )

    ListTopAppBar(
        openDrawer = { /* Abrimos un drawer */ },
        actionItems = actionItems
    )
}

Al correr la previsualización, si presionas el botón de los tres puntos, se despliegan las cinco acciones, las cuales responderán a tus clicks.

Abrir DropdownMenu en TopAppBar

Redimensión de pantalla: La solución anterior no se adapta al cambio de tamaño de pantalla. Pero puedes usar el mecanismo visto en Crear Layouts Para Tablets En Jetpack Compose (todo) para cubrir esa característica.


4. Top App Bar Prominente

La top app bar prominente extiende su altura para presentar títulos largos, proyectar imágenes o simplemente para manifestar la importancia de la misma en la pantalla.

Consigue este aspecto, modificando la altura de la TopAppBar con el parámetro modifier a través del modificador height(). Veamos un ejemplo.

4.1 Título De Doble Línea

Top App Bar prominente con título largo
Figura 8. Top App Bar prominente con título largo

Cuando el texto del título excede el espacio del contenido asignado, creamos una Top App Bar prominente que limite el texto a dos líneas.

¿Qué hacer?

Aumentemos la altura a 128dp y usemos el parámetro maxLines con el valor de 2 en el componente Text del título:

@Composable
fun ProminentTopAppBar() {
    TopAppBar(
        modifier = Modifier.height(ProminentTopAppBarHeight)
    ) {
        CompositionLocalProvider(
            LocalContentAlpha provides ContentAlpha.high
        ) {
            Row(
                modifier = Modifier
                    .width(68.dp)
                    .align(Alignment.Top) // (*)
            ) {
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(imageVector = Icons.Filled.Menu, contentDescription = "Abrir menú")
                }
            }
        }

        CompositionLocalProvider(
            LocalContentAlpha provides ContentAlpha.high,
            LocalTextStyle provides MaterialTheme.typography.h5
        ) {
            Text(
                text = "Top App Bar Prominente",
                maxLines = 2,
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
                    .align(Alignment.Bottom) // (*)
                    .padding(bottom = 16.dp)
            )
        }

        IconButton(
            onClick = { /*TODO*/ },
            modifier = Modifier.align(Alignment.Top) // (*)
        ) {
            Icon(imageVector = Icons.Filled.Search, contentDescription = "Buscar")
        }
        IconButton(
            onClick = { /*TODO*/ },
            modifier = Modifier.align(Alignment.Top) // (*)
        ) {
            Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "Ver más")
        }
    }
}

Como ves, usamos el constructor flexible para tomar control de las alineaciones verticales dentro del RowScope con el modificador align().

El título lo tendremos en en la parte inferior (Aligment.Bottom) y los iconos en la superior (Alignment.Top)

4.2 Imagen En Top App Bar Prominente

TopAppBar prominente con Imagen
Figura 9. TopAppBar prominente con Imagen

Tomemos otro ejemplo en donde la App Bar prominente contiene una imagen como se muestra en la figura anterior.

La solución es usar un layout Box para superponer el contenido general de la barra sobre una elemento Image que ocupa todo el tamaño:

@Composable
fun ProminentTopAppBarWithImage() {
    TopAppBar(
        modifier = Modifier.height(ProminentTopAppBarHeight),
        contentPadding = PaddingValues(all = 0.dp) // (1)
    ) {
        Box(modifier = Modifier.fillMaxSize()) { // (2)
            Image( // (3)
                painter = painterResource(id = R.drawable.image2),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxSize()
            )
            Row( // (4)
                modifier = Modifier
                    .fillMaxSize()
                    .padding(horizontal = 4.dp) // (5)
            ) {
                /*...*/
            }
        }
    }
}

Nota que:

  1. Pasamos 0dps en el contentPadding de la TopAppBar, ya que por defecto trae consigo 4dp horizontal. Esto haría que la imagen no ocupase todo el espacio
  2. La caja se expande por completo con fillMaxSize()
  3. El componente Image usa contentScale con Crop para extender por completo el contenido
  4. El contenido de la TopAppBar va en un layout Row para conservar la organización que conocemos
  5. Reponemos los 4dp en el contenido para ajustarnos a los lineamientos del Material Design

5. Personalizar La TopAppBar

Veamos algunos ejemplos sobre los últimos parámetros de TopAppBar. Pero antes, tomemos la función DetailTopAppBar y añadamos los parámetros backgroundColor, contentColor y elevation:

@Composable
internal fun DetailTopAppBar(
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: Dp = AppBarDefaults.TopAppBarElevation
) {
    TopAppBar(
        /*...*/,
        backgroundColor = backgroundColor,
        contentColor = contentColor,
        elevation = elevation
    )
}

Al elevar estos valores, tendremos un cuerpo reutilizable con el objetivo de presentar de manera escueta los siguientes casos.

Ejemplo: Cambiar Color De Fondo

TopAppBar con azul 500 en colorBackground
Figura 10. TopAppBar con azul 500 en colorBackground

Usa el parámetro backgroundColor para modificar el color de fondo de la TopAppBar a partir de una instancia Color.

Pongamos el caso de cambiar el color a un Azul 500:

@Composable
internal fun TopAppBarCustomBackground() {
    DetailTopAppBar(backgroundColor = Color(0xFF2196F3))
}

Ejemplo: Cambiar Color De Contenido

TopAppBar con amarillo 500 en contentColor
Figura 11. TopAppBar con amarillo 500 en contentColor

El color de contenido se refiere al color aplicado a los iconos y texto del contenedor de la barra superior.

Supongamos que deseamos cambiar el color de contenido a un Amarillo 500:

@Composable
internal fun TopAppBarCustomContentColor() {
    DetailTopAppBar(contentColor = Color(0xFFFFEB3B))
}

Ejemplo: Cambiar Elevación

TopAppBar con diferentes valores de elevation
Figura 12. TopAppBar con diferentes valores de elevation

Usa el parámetro elevation para modificar la elevación de la app bar.

En los lineamientos su valor por defecto es de 4dp, pero si por cuestiones de diseño deseas aplicar un valor diferente, tan solo pásalo como argumento:

@Composable
internal fun TopAppBarCustomElevation() {
    DetailTopAppBar(elevation = 8.dp, backgroundColor = Color.White)
}

6. Tematizar TopAppBar

Ya para finalizar, veamos que elementos modificar en un tema de Jetpack Compose con el propósito de alterar individualmente a nuestra TopAppBar.

Top App Bar con tema personalizado
Figura 13. Top App Bar con tema personalizado

Tomemos como ilustración el resultado de la figura 13, donde están definidos los siguientes aspectos:

  • Colores: primary para el fondo y onPrimary para el contenido
  • Tipografía: El estilo h6 para el título

Aplicar los anteriores parámetros requiere del uso de la clase MaterialTheme y sus parámetros typographic y colors.

private val Besley = FontFamily(Font(R.font.besley_medium))

@Composable
internal fun ThemedTopAppBar() {

    MaterialTheme(
        colors = MaterialTheme.colors.copy(
            primary = Color(0xFFFFF8E1),
            onPrimary = Color(0xFF3e2723)
        ),
        typography = MaterialTheme.typography.copy(
            h6 = TextStyle(fontFamily = Besley, fontSize = 21.sp)
        )
    ) {
        DetailTopAppBar()
    }
}

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!