Snackbar En Android

La Snackbar en Android es un elemento parecido al Toast, ya que muestra un mensaje emergente al usuario para informar sobre una acción realizada. Pero adicionalmente puede mostrar un botón al usuario para interactuar con el resultado percibido.

Visual de una Snackbar en Android

En este tutorial veremos cómo usar a clase Snackbar, uno de los componentes de la librería de Material Design de Google para comunicar al usuario acciones realizadas si tu app está en primer plano. Acciones como confirmar operaciones sobre registros, peticiones web terminadas, cambios de estados en servicios del sistema, etc.

Para interiorizar las funcionalidades generales de este widget, usaremos como referencia la siguiente App de ejemplo:

App de ejemplo con Snackbar

En ella usaremos una Snackbar para notificar en el momento en que añade un registro a una lista. Adicionalmente proveeremos la habilidad de deshacer la inserción del último registro. Puedes descargar el proyecto de Android estudio desde el siguiente enlace:

Crear Y Mostrar Una Snackbar En Android

En nuestro ejemplo mostramos la siguiente Snackbar con el mensaje de que un registro de texto ha sido agregado a la lista:

Crear y mostrar SnackBar

Para lograr este resultado primero debes añadir la dependencia a material components:

dependencies {
    
    implementation 'com.google.android.material:material:ultima-version'
}

Luego usa alguno de los métodos de fabricación make() para crear la Snackbar y el método show() para mostrarla.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Snackbar.make(
            findViewById(R.id.textview),
            "Registro guardado",
            BaseTransientBottomBar.LENGTH_SHORT
        ).show()
    }
}

Donde los argumentos del método make() cumplen el siguiente propósito:

  • context: El contexto usado para crear el view de la Snackbar
  • view: Un view usado como referencia para buscar sobre el árbol de jerarquía hasta encontrar un padre adecuado en donde añadir la Snackbar. Por adecuado se entiende que sea de tipo CoordinatorLayout o el view de primer nivel de la ventana.
  • text: El recurso string o secuencia de caracteres a mostrar como mensaje.
  • duration: La cantidad de milisegundos que se mostrará el widget. Puedes usar las constantes LENGTH_LONG, LENGTH_SHORT y LENGTH_INDEFINITE; o también un tiempo personalizado.

Añadir Acción A Una Snackbar

Con nuestra app de registros permitimos al usuario deshacer la agregación de registros a través del botón de acción «DESHACER».

Snackbar con acción deshacer

Para agregar esta acción a la Snackbar usa el método setAction(). El primer parámetro será el nombre de la acción y el segundo un ejemplar que implemente el observador View.OnClickListener:

private fun setUpSnackbar() {
    snackbar = Snackbar.make(
        findViewById(R.id.constraint_layout),
        "Registro guardado",
        BaseTransientBottomBar.LENGTH_LONG
    )
    snackbar.setAction("Deshacer") {
        saveCommand.undo()
        updateLogsList()
    }
}

private fun updateLogsList() {
    val logView: TextView = findViewById(R.id.log_view)
    logView.text = logs.joinToString(separator = "\n")
}

Al interior de la escucha de la acción en la Snackbar, invocamos al método undo() de un pequeño comando que hemos creado (LogCommand) para encapsular el guardado de cada registro:

interface LogCommand {
    fun saveLog(log: String)
    fun undo()
}

class SaveLogCommand(private val logs: MutableList<String>) : LogCommand {
    private var lastLog: String? = null

    override fun saveLog(log: String) {
        val sanitizeLog = if (log.isBlank()) "[vacío]" else log
        lastLog = sanitizeLog
        logs.add(sanitizeLog)
    }

    override fun undo() {
        logs.remove(lastLog)
    }
}

Este tiene como propiedad a una lista mutable de strings, la cual actúa como el recibidor del comando. Con saveLog() guardados un registro y lo retenemos como el último disponible.

private fun saveLog() {
    val log = getLogText()

    saveCommand.saveLog(log)

    updateLogsList()
    hideKeyboard()
    buildSnackbar()
}

private fun updateLogsList() {
    val logView: TextView = findViewById(R.id.log_view)
    logView.text = logs.joinToString(separator = "\n")
}

Y con undo() removemos al último registro.

Incrementar Funcionalidad Con El CoordinatorLayout

Si despliegas una Snackbar sobre un CoordinatorLayout esta ganará comportamientos adicionales debido a la interacción con él. Por ello en el ejemplo usamos esta jerarquía en el layout de la actividad principal.

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/constraint_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <include layout="@layout/main_content" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/save_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        app:srcCompat="@drawable/ic_save"
        android:contentDescription="Guardar" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Deslizar Para Descartar Snackbar

Una de las posibilidades otorgadas, es la capacidad de desvanecer a la Snackbar con un swipe horizontal:

Deslizar para desvanecer Snackbar

Floating Action Button Y Snackbar

Si en el CoordinatorLayout existe un FloatingActionButton y se muestra una Snackbar, el FAB se desplazará hacia arriba para dar paso momentáneamente a la aparición de mensaje. Una vez desvanecida, el FAB recobrará su posición original.

FAB se mueve al aparecer una Snackbar

Cabe destacar que en la sección de lineamientos para el desplazamiento del componente, nos recomiendan dejar al FAB intacto y mostrar la Snackbar por encima.

Anclar Snackbar a FAB

Esto puedes lograrlo asignando el ID del FAB para asignarlo como «ancla» con el método setAnchorView().

private fun setUpSnackbar() {
    Snackbar.make(/**/,/**/,/**/)
    .setAction("Deshacer") {
        // ...
    }.setAnchorView(R.id.save_fab)
}

Personalizar La Snackbar

Nuestra app tiene un control Switch para establecer el estilo aplicado a las Snackbars en la parte superior. Cuando lo activamos, estas comienzan a mostrarse con la siguiente apariencia:

Snackbar con color personalizado

Y es que la clase Snackbar posee varias propiedades mutables y métodos set*() para cambiar atributos como: color del texto en el mensaje, color de background, texto de la acción, etc.:

private fun buildSnackbar() {
    // ...
    if (useStyle) {
        snackbar.setBackgroundTint(color(R.color.purple_700))
        snackbar.setActionTextColor(color(R.color.purple_200))
    } else {
        snackbar.setBackgroundTint(color(R.color.teal_700))
        snackbar.setActionTextColor(color(R.color.teal_200))

    }
    snackbar.show()
}

private fun setUpSwitch() {
    val switch: SwitchMaterial = findViewById(R.id.style_switch)
    switch.setOnCheckedChangeListener { _, isChecked ->
        useStyle = isChecked
    }
}

private fun color(color: Int) = ContextCompat.getColor(this, color)

En el código anterior usamos los métodos setBackgrountTint() para asignar el color del background del view de la snackbar. También invocamos a setActionTextColor() para modificar el color del botón de acción.

Aplicar Un Tema

Si deseas aplicar un mismo tema a todas las snackbars que aparezcan a lo largo de tu aplicativo, entonces puedes usar los atributos snackbarStyle, snackbarButtonStyle y snackbarTextViewStyle para modificar la apariencia general, la del botón de acción y el texto:

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.SnackbarEnAndroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        
        <item name="snackbarStyle">@style/Widget.App.Snackbar</item>
        <item name="snackbarButtonStyle">@style/Widget.App.SnackbarButton</item>
    </style>


    <style name="Widget.App.Snackbar" parent="Widget.MaterialComponents.Snackbar">
        <item name="backgroundTint">@color/teal_700</item>
        <item name="actionTextColorAlpha">1</item>
    </style>

    <style name="Widget.App.SnackbarButton" parent="Widget.MaterialComponents.Button.TextButton.Snackbar">
        <item name="android:textColor">@color/teal_200</item>
    </style>
</resources>

El resultado producido por la modificación anterior a los colores del background y la apariencia del texto, sería el siguiente:

Cambiar tema XML para Snackbar

Observar Aparición Y Desaparición

Nuestra App de registro también cuenta la cantidad de veces que se ha deshecho una acción de inserción:

Usar addCallback() sobre Snackbar

Esto lo logramos usando al observador Snackbar.Callback, el cual provee dos métodos para procesar el momento en que aparece (onShow()) y desparece (onDismissed()) la Snackbar que emite las notificaciones:

snackbar.addCallback(object : Snackbar.Callback() {
    override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
        if (event == DISMISS_EVENT_ACTION) {
            counter++
            updateTextCounter()
        }
    }
})

private fun updateTextCounter() {
    counterText.text = "Cantidad de inserciones deshecha: $counter"
}

En el anterior código creamos una expresión de objeto anónimo para crear la instancia de Snackbar.Callback. Al sobrescribir onDismissed() usamos el parámetro event para determinar el origen de la causa del descarte de la Snackbar.

Buscamos solo los eventos relacionados al click del usuario en DESHACER, por lo que usamos la constante DISMISS_EVENT_ACTION y luego actualizamos el texto del contador.

Claramente existen otros valores para diferentes causas:

  • DISMISS_EVENT_CONSECUTIVE: Cuando la Snackbar se descarta por la aparición de otra
  • DISMISS_EVENT_MANUAL: Cuando llamaste manualmente al método dismiss()
  • DISMISS_EVENT_SWIPE: Cuando deslizaste la Snackbar para desvanecerla
  • DISMISS_EVENT_TIMEOUT: Cuando la Snackbar se desvaneció sola

Al momento de juntar todas estas características la aplicación permite guardar registros en una lista y deshacer el último comando inserción.

Ejemplo de Snackbar en Android con acción deshacer

Ú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!