Índice de contenido
- ¿Qué es el Snackbar?
- Introducción al Snackbar en Compose
- Estado del Snackbar
- ️ El Scaffold: Nuestro Lienzo Estructural
- Estados y Corrutinas
- ¿Por qué necesitamos Corrutinas?
- Implementación
- El resultado de la acción
- Resumen de la estructura
- ¿Por qué mover el Scaffold a un Composable?
- Ejemplo de FloatingActionButton y Snackbar
- El Snackbar (Enfoque Legacy)
- ¿Cómo emplear el Snackbar en una aplicación Android (Enfoque Legacy)?
El Snackbar es esta pequeña barra que seguramente ya has visto muchas veces. Por ejemplo, cuando envías un correo y aparece un mensaje abajo preguntando si quieres deshacer la acción o simplemente confirmando que la operación se realizó correctamente.
Es una especie de diálogo de confirmación, pero mucho más ligero:
- Una barra sencilla que aparece en la parte inferior de la pantalla durante unos pocos segundos para confirmar una acción o permitir revertirla, usualmente aparece cuando creamos un listado en Android Studio | Jetpack Compose al momento de hacer alguna operación.
¿Qué es el Snackbar?
Siguiendo hablando un poco de experiencias personales, vi por primera vez este nuevo elemento llamado Snackbar en las últimas versiones publicadas de Gmail las cuales venían poco a poco estableciendo características del Material Design:

Introducción al Snackbar en Compose
Su implementación en Android moderno (Compose) es un poco "truculenta" y no tan directa como parece. Lo primero que debemos entender es que se divide en dos bloques principales: el Host (donde se aloja y muestra) y el Estado (la lógica que lo activa).
Estado del Snackbar
Aquí sucede algo similar a lo que vimos con los diálogos.
- Tenemos una variable de estado llamada SnackbarHostState, que es una estructura ya “pre-cocinada” que nos provee Compose.
- Esta variable se crea usando remember, lo que nos permite conservar su estado entre recomposiciones.
- Este estado luego se pasa como parámetro al Scaffold, específicamente en la propiedad snackbarHost.
Finalmente su uso mediante una corrutina para mostrar el snackbar:
scope.launch {
val result = snackbarHostState.showSnackbar(
message = "Elemento eliminado",
actionLabel = "Deshacer"
)
when (result) {
SnackbarResult.ActionPerformed -> { /* Lógica para restaurar el cambio */ }
SnackbarResult.Dismissed -> { /* Lógica cuando desaparece solo */ }
}
}️ El Scaffold: Nuestro Lienzo Estructural
Para mostrar el Snackbar, necesitamos el Scaffold. Este componente me gusta mucho porque hereda la filosofía de Flutter: es un "lienzo precocinado" que define la estructura fundamental de nuestra aplicación.
Si revisamos la definición del Scaffold, veremos que ya tiene espacios reservados para:
- TopBar: La barra superior.
- BottomBar: La barra de navegación inferior.
- FloatingActionButton: El botón flotante.
- SnackbarHost: El lugar específico donde aparecerá nuestro Snackbar.
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit,
)Antes definíamos el Scaffold directamente en el onCreate, pero ahora lo movemos a una función @Composable propia para poder manejar la reactividad y los estados, permitiendo que la interfaz reaccione cuando queramos mostrar u ocultar la barra.
Estados y Corrutinas
Para que el Snackbar funcione, necesitamos declarar variables de estado dentro de nuestro Composable. Usamos remember para que el estado persista durante las recomposiciones:
- SnackbarHostState: Es la estructura que referencia al host dentro del Scaffold.
- CoroutineScope: Aquí es donde entra la parte asíncrona.
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()Estas variables deben ser declaradas dentro de una función composable para poder ser declaradas, es decir, NO la puedes colocar por ejemplo, dentro del onCreate:
Functions which invoke @Composable functions must be marked with the @Composable annotation
Functions which invoke @Composable functions must be marked with the @Composable annotationclass MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
super.onCreate(savedInstanceState)¿Por qué necesitamos Corrutinas?
Mostrar un Snackbar es una operación de tipo suspend (asíncrona). Es equivalente a las funciones async/await en JavaScript o Python. Como la barra debe mostrarse por un tiempo determinado (3 o 4 segundos) y luego desaparecer, no podemos ejecutarla en el hilo principal porque congelaría la aplicación, arruinando la experiencia del usuario.
El compilador te dará un error si intentas mostrar un Snackbar fuera de un ámbito de corrutina (scope.launch). Al lanzarlo en una corrutina, el proceso corre de fondo sin bloquear la interfaz.
scope.launch {
val result = snackbarHostState.showSnackbar(
message = "Elemento eliminado",
actionLabel = "Deshacer"
)
when (result) {
SnackbarResult.ActionPerformed -> { /* Lógica para restaurar el cambio */ }
SnackbarResult.Dismissed -> { /* Lógica cuando desaparece solo */ }
}
}Implementación
Dentro de nuestro botón (por ejemplo, al eliminar un elemento de una lista), lanzamos la corrutina para mostrar el Snackbar:
package com.example.myproyectandroid
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.myproyectandroid.ui.theme.MyProyectAndroidTheme
import kotlinx.coroutines.launch
import com.example.myproyectandroid.CardActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyProyectAndroidTheme {
PantallaPrincipal()
}
}
}
}
@Composable
fun PantallaPrincipal() {
// 1. Las variables de ESTADO se colocan AQUÍ (dentro del Composable)
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
// 2. El Scaffold organiza la estructura visual
Scaffold(
snackbarHost = {
// El Host es el "escenario" donde aparecerá el Snackbar
SnackbarHost(hostState = snackbarHostState)
}
) { paddingValues ->
// 3. El contenido principal
// Usamos paddingValues para que el contenido no quede debajo de la TopBar o Snackbar
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(onClick = {
// 4. Lanzamos la corrutina (el "hilo" asíncrono)
scope.launch {
val result = snackbarHostState.showSnackbar(
message = "Elemento eliminado",
actionLabel = "Deshacer",
duration = SnackbarDuration.Short
)
// Opcional: Manejar la acción del botón "Deshacer"
when (result) {
SnackbarResult.ActionPerformed -> {
/* Aquí pondrías la lógica para restaurar el item */
print("ActionPerformed")
Log.d("MI_APP", "ActionPerformed")
}
SnackbarResult.Dismissed -> { /* Se cerró solo */
print("Dismissed")
Log.d("MI_APP", "Dismissed")
}
}
}
}) {
Text("Mostrar Snackbar")
}
}
CardBonito()
}
}
@Preview(showBackground = true)
@Composable
fun Preview() {
MyProyectAndroidTheme {
PantallaPrincipal()
}
}El resultado de la acción
Al ser una función asíncrona, podemos capturar el resultado. Si el usuario hace clic en "Deshacer" (ActionPerformed), ejecutamos la lógica correspondiente; si simplemente se ignora y desaparece (Dismissed), el proceso termina.
Para verificar esto, puedes usar Log.d (la etiqueta de Logcat con Android Studio | Jetpack Compose) para ver los mensajes en consola cuando el usuario interactúa con la barra:
scope.launch {
val result = snackbarHostState.showSnackbar(
message = "Elemento eliminado",
actionLabel = "Deshacer"
)
when (result) {
SnackbarResult.ActionPerformed -> {
Log.d("MI_APP", "ActionPerformed")
}
SnackbarResult.Dismissed -> {
Log.d("MI_APP", "Dismissed")
}
}
}Resumen de la estructura
En resumen, tenemos:
- El SnackbarHost, que es el lugar donde se va a renderizar.
- El estado (SnackbarHostState), que controla cuándo se muestra.
- Una corrutina, que se encarga de ejecutar la función asíncrona para mostrar el Snackbar.
¿Por qué mover el Scaffold a un Composable?
Para cerrar, la razón principal por la que movimos el Scaffold es que necesita estar dentro de un Composable.
Cuando estaba definido directamente en el ciclo de vida de la actividad, no podíamos manejar estados ni corrutinas correctamente.
Al moverlo a un Composable, ahora sí podemos trabajar con estado reactivo, corrutinas y toda la lógica moderna de Jetpack Compose.
Y con eso, ya tenemos nuestra implementación de Snackbar funcionando correctamente.
Ejemplo de FloatingActionButton y Snackbar
También, podemos usar un FAB definido en el Scaffold:
fun PantallaPrincipal() {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text("Show snackbar") },
icon = { Icon(Icons.Filled.Image, contentDescription = "") },
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Snackbar")
}
}
)
}
) { contentPadding ->
// Screen content
}
}El Snackbar (Enfoque Legacy)
En este apartado se centra en la implementación de elementos del Material Design en Android utilizando el enfoque Legacy (basado en la Librería de Soporte y el sistema de Vistas tradicional). Aunque los conceptos de Material Design han evolucionado y se implementan de forma nativa en Jetpack Compose (el enfoque Composable moderno), es fundamental entender cómo se abordaban en el sistema de vistas. A continuación, exploraremos cómo integrar componentes como el Snackbar en este contexto.
En el enfoque Legacy, el Snackbar es una evolución del conocido Toast para mostrar mensajes. Ambos elementos han estado presentes desde los inicios de Android, con el Toast siendo un mensaje simple y el Snackbar ofreciendo más interactividad.

Tanto el Toast como el Snackbar presentan grandes similitudes en su aspecto y comportamiento. Sin embargo, en el enfoque Legacy, el Snackbar se distingue por permitir acciones (como un botón de "Deshacer") asociado a un evento OnClickListener, lo que permite al usuario revertir acciones. Esta interactividad es clave y Google la emplea en aplicaciones como Gmail.
En el enfoque Composable de Jetpack Compose, el Toast sigue existiendo con un propósito similar (mensajes cortos y no interactivos), mientras que el Snackbar se gestiona a través de SnackbarHost y SnackbarHostState, ofreciendo la misma funcionalidad interactiva pero de una manera declarativa y más integrada en la jerarquía de la UI de Compose.
¿Cómo emplear el Snackbar en una aplicación Android (Enfoque Legacy)?
Para emplear el Snackbar en el enfoque Legacy (basado en Vistas), se utiliza la clase com.google.android.material.snackbar.Snackbar. Lo construimos a través del método estático make() que consta de los siguientes parámetros: make(View view, int resId, int duration).
El parámetro view es la vista padre; de nuestra aplicación, la cual el Snackbar utilizará para encontrar el lugar adecuado donde mostrarse.
La duración duration tiene establecidas una serie de constantes que controlan cuánto tiempo permanece visible el Snackbar:
Constantes de Duración (com.google.android.material.snackbar.Snackbar) | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| int | LENGTH_INDEFINITE | Muestra el Snackbar indefinidamente hasta que sea cerrado manualmente. | |||||||||
| int | LENGTH_LONG | Muestra el Snackbar por un período largo de tiempo. | |||||||||
| int | LENGTH_SHORT | Muestra el Snackbar por un período corto de tiempo. | |||||||||
El parámetro resId es el recurso de un texto o un String directo para el mensaje.
También cuenta con un método setAction(CharSequence text, android.view.View.OnClickListener listener) en el cual se especifica el texto y el comportamiento (Listener) del botón de acción.
Finalmente, para mostrar el Snackbar, se invoca el método show(). Veamos el código de ejemplo completo para el enfoque Legacy:
com.google.android.material.snackbar.Snackbar.make(layout,"Audio deleted", com.google.android.material.snackbar.Snackbar.LENGTH_LONG) .setAction("Undo", snackbarClickListener) .show();Definimos el evento Listener del botón:
View.OnClickListener snackbarClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// el usuario presiono el boton, revirtio el proceso
Log.i("Snackbar","Botón presionado");
}
};Si nuestro layout (en el enfoque Legacy) incluyera un FloatingActionButton, su definición en XML sería similar a la siguiente. Es importante notar el cambio del paquete android.support.design.widget a com.google.android.material.floatingactionbutton en implementaciones modernas de las Views.
"http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/record_fab"
style="@style/FloatingActionButtonStyle"
android:src="@mipmap/ic_av_mic"
app:backgroundTint="@color/color_primary"
app:layout_anchor="@id/audio_rv"
app:layout_anchorGravity="bottom|right|end" />
En el enfoque Composable (Jetpack Compose), un FloatingActionButton se implementaría utilizando el Composable androidx.compose.material.FloatingActionButton, a menudo dentro de un Scaffold, lo que simplifica su posicionamiento y comportamiento en la UI.

Es fundamental recordar las directrices del Material Design de Google, que especifican cómo los elementos deben interactuar y posicionarse en la UI para ofrecer una experiencia de usuario consistente y agradable. Por ejemplo, evitar el solapamiento de un Snackbar con un FloatingActionButton sin una transición adecuada es una de estas reglas, aplicable tanto en el enfoque Legacy como en el Composable para mantener la coherencia visual. Explorar estas transiciones y comportamientos avanzados será tema de futuras discusiones.