Interacciones De Notificaciones En Android

En este segmento aprenderás a manejar las interacciones de notificaciones en Android según las formas en que el usuario puede tener interacciones con ellas.

Concretamente, verás ejemplos para:

  • Iniciar una actividad al tocar una notificación
  • Expandir el contenido de una notificación
  • Descartar una notificación
  • Controlar los ajustes de la notificación
  • Manejar clicks de los botones de acción
  • Resumir notificaciones en grupos

Nota: Este tutorial es la segunda parte de la guía de notificaciones (todo), por lo que asumiré que ya leíste la primera. Si no es así, ve a Crear Notificaciones En Android.

Ejemplo De Interacciones De Notificaciones En Android

Actualizaremos el proyecto existente de notificaciones para procesar las acciones relacionadas con la notificación que hemos creado. Crearemos un segundo módulo llamado P2_Interacciones para incluir el código Kotlin relacionado con las interacciones.

Puedes descargar el proyecto Android Studio con el código de solución desde el siguiente enlace:


Iniciar Actividad Al Tocar Notificación

Supongamos que deseamos abrir una actividad con el detalle del tutorial que muestra en su contenido la notificación de ejemplo:

Iniciar actividad desde notificación en Android

La forma de manejar este evento es con el uso del método setContentIntent() en la construcción de la notificación. Este recibe un PendingIntent que es enviado cuando el usuario hace click en ella.

Así que modificaremos nuestro método ya existente generateNotification() para agregar este atributo y además le cambiaremos la firma para recibir el ID de la notificación, el título y la descripción del post:

private fun generateNotification(
    notificationId: Int,
    title: String,
    description: String
) {
    val contentIntent = Intent(this, PostDetailActivity::class.java).apply {// (1)
        putExtras(// (2)
            bundleOf( // (3)
                "notification_id" to notificationId,
                "title" to title,
                "body" to description
            )
        )
    }
    val contentPendingIntent: PendingIntent? = TaskStackBuilder.create(this).run { // (4)
        addNextIntentWithParentStack(contentIntent) // (5)
        getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // (6)
    }

    val notification = NotificationCompat.Builder(this, CHANNEL_ID)
        //..
        .setContentIntent(contentPendingIntent) // (7)
        //..
        .build()
}

En el código anterior:

  1. Creamos un intent explícito hacia PostDetailActivity e invocamos la función apply() para configurar sus atributos.
  2. Llamamos a putExtras() para enviar los valores que necesita la actividad de detalle.
  3. Usamos la función bundleOf() para crear extras a partir de instancias Pair.
  4. Creamos un nuevo PendingIntent para inicio de actividad con el TaskBuilder. Invocamos a run() para configurar y asignar al mismo tiempo:
  5. El método addNextIntentWithParentStack() crea un intent que asocia a PostDetailActivity con MainActivity, con el fin de presionar el up button y retornar al padre.
  6. getPendingIntent() obtiene el PendingIntent asociado a la construcción producida hasta el momento dentro del task builder. Recibe como argumentos:
    • requestCode: Código de petición asociado al envío
    • flags: Valores para personalizar el proceso de envío del intent. Usaremos FLAG_UPDATE_CURRENT para indicar la existencia del pending intent y así actualizar los extras en cada transmisión.
  7. Invoca a setContentIntent() desde NotificationCompat.Builder y pasamos a contentPendingIntent.

Crear Actividad De Resultado De Notificación

Añade al proyecto una nueva actividad llamada PostDetailActivity desde File > New > Activity > Empty Activity.

El diseño del layout será sencillo. Presentará tres textos en línea vertical para mostrar la categoría del post, el título y su contenido:

Layout para actividad de resultado de notificaciones

La definición XML será la siguiente:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".PostDetailActivity">

    <TextView
        android:id="@+id/post_category"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="16dp"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Overline"
        android:textStyle="bold"
        tools:text="@tools:sample/lorem" />

    <TextView
        android:id="@+id/post_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingBottom="32dp"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
        tools:text="@tools:sample/lorem" />

    <TextView
        android:id="@+id/post_body"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
        tools:text="@tools:sample/lorem/random" />
</LinearLayout>

Ahora, desde la clase se obtendrán a todos los extras del post para obtener el detalle completo de su contenido:

class PostDetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_post_detail)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        discardNotifications()
        setUpContent()
    }

    private fun discardNotifications() {
        val notificationManager = ContextCompat.getSystemService(
            this,
            NotificationManager::class.java
        ) as NotificationManager
        notificationManager.cancelAll()
    }

    private fun setUpContent() {
        val title = intent.getStringExtra("title")
        val body = intent.getStringExtra("body")

        findViewById<TextView>(R.id.post_category).text = "Android > UI > Notificaciones"
        findViewById<TextView>(R.id.post_body).text = body
        findViewById<TextView>(R.id.post_title).text = title
    }
}

Este ejemplo usó una actividad que deseamos provea navegación hacia la actividad principal, por lo que el TaskBuilder te permite la construcción de la back stack.

No obstante, si la actividad relacionada a la notificación no pertenece a una jerarquía de pantallas, entonces crea el PendingIntent como se indica en esta guía oficial.


Notificaciones Expandibles

Las notificaciones tienen la capacidad de expandirse si el tamaño del view por defecto es excedido. Este comportamiento lo vimos cuando añadimos una acción a nuestra notificación, donde Android provee un indicador para expandir/contraer para ver la acción:

Notificaciones expandibles en Android

Aunque también puedes usar el gesto de deslizamiento vertical para revelar el cuerpo adicional.

Otra forma de proveer la capacidad de expansión son las plantillas. En el tutorial de Plantillas de notificaciones (todo) verás que existen diferentes estilos que te permitirán mostrar contenido adicional en la notificación.

Por ejemplo, si quisiéramos mostrar un bloque grande de texto al ampliar la notificación usamos el método setStyle() con la clase auxiliar BigTextStyle de la siguiente forma:

val style = NotificationCompat.BigTextStyle().bigText(getString(R.string.post_big_text))

val notification = NotificationCompat.Builder(this, channelId)
    //..
    .setStyle(style)
    //..
    .build()

El método bigText() recibe la secuencia de caracteres asociada al texto largo que deseamos mostrar al expandir la notificación. Para nuestro caso, será la descripción del post.


Descartar Una Notificación

Los usuarios pueden descartar notificaciones con un gesto de swipe lateral desde el notifications drawer como se ilustra a continuación:

Descartar notificaciones en Android

Adicionalmente el framework nos proporciona varios métodos para descartarlas desde nuestro código Kotlin:

  • setAutoCancel(): Manifiesta que deseas que la notificación sea descartada cuando el usuario haga tap en su contenido. Puedes usarlo en conjunto con setDeleteIntent() para notificar a otro componente que este evento se consumió.
  • cancel(): Cancela la notificación previamente desplegada con el ID pasado como argumento.
  • cancelAll(): Descarta a todas la notificaciones previamente mostradas. Este lo usamos en discardNotifications() de PostDetailActivity para cuando usemos grupos más adelante.
  • setTimeOutAfter(): Especifica la cantidad de milisegundos en la que la notificación debe ser descartada (tiene efecto solo Android Oreo hacia arriba)

Sabiendo esto, podemos agregar a setAutoCancel(true) en la construcción de nuestra notificación con el fin de desvanecerla cuando el usuario haga click en ella:

val notification = NotificationCompat.Builder(this, channelId)
    //..
    .setContentIntent(pendingIntent)
    .setAutoCancel(true)
    //..
    .build()

Notificaciones Ongoing

En el caso que desees que la notificación represente un proceso continuo y quieras evitar el descarte por swipe, entonces usa el método setOngoing() con true:

val notification = NotificationCompat.Builder(this, channelId)
    //..
    .setOngoing(true)
    //..
    .build()

Controles De Notificación

Cuando haces un click prolongado sobre la notificación, Android mostrará una interfaz emergente con acciones que controlan la aparición de tus notificaciones:

Mostrar ajustes de notificaciones con click prolongado

También puedes acceder a la configuración con el icono de ajustes que aparece al swipear lateralmente:

Esto te permitirá modificar aspectos asociados a la notificación y las configuraciones proveídas por el canal que las contiene. Veremos más sobre estos ajustes en el tutorial de configuración de notificaciones (todo).


Manejar Click De Los Botones De Acción

Habíamos visto que desde el builder de notificaciones podemos usar el método addAction() para añadir un botón de acción. Como ilustración usamos un botón para «Leer más tarde» indicando un hipotético modelo de marcadores para los posts:

Click en botones de acción de notificaciones en Android

Pasar PendingIntent A AddAction()

Aunque creamos el botón, no especificamos una lógica. Para complementar la acción reemplazamos el tercer parámetro que era null, por un PendingIntent:

val bookmarkIntent = Intent(applicationContext, BookmarkReceiver::class.java) // (1)
val bookmarkPendingIntent = PendingIntent.getBroadcast( // (2)
    applicationContext,
    notificationId,
    bookmarkIntent,
    PendingIntent.FLAG_UPDATE_CURRENT
)

val notification = NotificationCompat.Builder(this, channelId)
    //..
    .addAction(R.drawable.ic_bookmark, "Leer más tarde", bookmarkPendingIntent) // (3)
    .build()

En el código anterior:

  1. Creamos un Intent explícito para especificar al componente BookmarkReceiver. Este será un BroadcastReceiver que procesa el evento de click en el botón.
  2. Creamos un PendingIntent desde el método getBroadcast() y pasamos el contexto de la aplicación y a bookmarkIntent.
  3. Pasamos como argumento a bookmarkPendingIntent en el método addAction() del botón

Crear BroadcastReceiver

Como ves, BookmarkReceiver es el encargado de recibir el click del usuario en el botón. Su cometido es incrementar el contador de marcadores que se han añadido a una fuente de datos en memoria:

class BookmarkReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        BookmarksStore.addBookmark()
        val notificationId = intent.getIntExtra("notification_id", -1)

        with(NotificationManagerCompat.from(context)) {
            cancel(notificationId)
        }
    }
}

La fuente es una declaración de objeto con solo dos métodos para la consulta y actualización del número de marcadores:

object BookmarksStore {

    var bookmarks: Int = 0
    private set

    fun addBookmark() = bookmarks++
}

Nota: Aunque estamos usando un BroadcastReceiver para procesar esta acción, también es posible iniciar actividades y servicios si así lo requieres.

Actualizar Interfaz De Usuario

Por último representaremos la repercusión de la acción del usuario en la UI a través de un TextView que proyecte la cantidad de artículos enviados a los marcadores:

Layout para mostrar resultado de notificaciòn

Para ello actualizaremos nuestro layout activity_main.xml y añadiremos el texto por encima del botón de notificación. La definición XML de la jerarquía será:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.develou.p2_interacciones.MainActivity">

    <Button
        android:id="@+id/notification_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/create_notification"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/bookmarks_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
        app:layout_constraintBottom_toTopOf="@+id/notification_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Marcadores: 0" />

</androidx.constraintlayout.widget.ConstraintLayout>

Ahora abre MainActivity y asigna el valor del contador de marcadores en onWindowFocusChanged():

override fun onWindowFocusChanged(hasFocus: Boolean) {
    if (hasFocus) {
        findViewById<TextView>(R.id.bookmarks_text).apply {
            text = getString(R.string.bookmarks, BookmarksStore.bookmarks)
        }
    }
}

De esta forma, cada que el botón «Leer más tarde» sea presionado, tendremos el nuevo número de marcadores.

Nota: Reemplaza la sobrescritura de onWindowFocusChanged() por un estado de vista observable al interior de un ViewModel para MainActivity.


Agrupar Notificaciones

Android te permite crear una jerarquía de notificaciones que estén relacionadas por una clasificación común. Esto genera una notificación padre que resume al grupo. Al ser expandida, se revelan a todos los hijos con sus respectivos contenidos y a su vez cada hijo puede ser expandido:

Grupo de notificaciones en Android

Para crear un grupo de notificaciones:

  1. Añade las notificaciones hijas al nuevo grupo con setGroup()
  2. Crea una notificación padre que actué como resumen del grupo y añádela con setGroup()
  3. Oficializa a la notificación padre como cabecera del grupo con setSummaryGroup()

Con esto en mente, creamos dos notificaciones al presionar el botón de la actividad y luego aplicamos la agrupación. Para ello modifiquemos primero el método generateNotification() para que reciba como parámetro los valores del cuerpo de la notificación:

private fun generateNotification(
    notificationId: Int,
    title: String,
    description: String
) {   
    //...
    val notification = NotificationCompat.Builder(this, CHANNEL_ID)
        //...
        .setGroup(GROUP_POSTS_KEY)
        .build()
    //...
}

Luego crearemos un nuevo método para generar la notificación representante del grupo:

private fun groupNotifications() {
    val group = NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_circle_notifications)
        .setSubText("Develou.com")
        .setGroup(GROUP_POSTS_KEY)
        .setGroupSummary(true)
        .build()

    showNotification(GROUP_ID, group)
}

Como ves, groupNotifications() crea una notificación común, solo que le indicamos que sirva como resumen de aquellas que pertenezcan al grupo con la clave GROUP_POSTS_KEY.

class MainActivity : AppCompatActivity() {
    private val GROUP_POSTS_KEY = "post_group"
    // ...
}

Y claramente, actualizamos las sentencias en el manejo del click del botón para crear dos notificaciones de inmediato. Esto nos simplificará la visibilidad del grupo:

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

    CHANNEL_ID = getString(R.string.basic_channel_id)

    findViewById<Button>(R.id.notification_button).setOnClickListener {
        generateNotification(
            1,
            "1. Crear notificaciones",
            getString(R.string.post_big_text)
        )
        generateNotification(
            2,
            "2. Interacciones de notificaciones",
            getString(R.string.post2_big_text)
        )
        groupNotifications()
    }

    createNotificationChannel()
}

De esta forma podrás agrupar las notificaciones y permitir al usuario expandir tanto a nivel de padre como de hijos:

Expandir grupos de notificaciones en Android

Configurar Notificaciones

En este tutorial viste varios ejemplos para manejar las acciones relacionadas a las notificaciones como: tap en el contenido, descarte, click en botones de acción, expansión y agrupación.

Lo siguiente será comprender sobre la Configuración de notificaciones (todo) y como el ajuste de sus canales, prioridades, categorías e importancia pueden cambiar su aparición ante el usuario.


Más Contenidos Android

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