Skip to content

Android Notification Action — Complete Guide

DodaTech Updated 2026-06-24 2 min read

In this tutorial, you'll learn about Android Notification Action. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

The Problem

Your notification action buttons don't respond to taps, or clicking "Reply" opens a new Activity instead of showing an inline reply.

Wrong Approach ❌

// Action that opens an Activity instead of inline reply
val replyIntent = Intent(this, ReplyActivity::class.java)
val replyPendingIntent = PendingIntent.getActivity(this, 0, replyIntent, FLAG_IMMUTABLE)

val notification = NotificationCompat.Builder(this, CHANNEL_ID)
    .addAction(R.drawable.ic_reply, "Reply", replyPendingIntent) // Opens new Activity — bad UX
    .build()

Output: User taps "Reply" and navigates away from current app. No inline reply.

Right Approach ✅

class MessagingService : FirebaseMessagingService() {

    private fun showReplyNotification(message: Message) {
        // RemoteInput for inline reply
        val remoteInput = RemoteInput.Builder("reply_key")
            .setLabel("Reply to ${message.sender}")
            .build()

        // Reply intent — handled by a BroadcastReceiver
        val replyIntent = Intent(this, ReplyReceiver::class.java).apply {
            putExtra("message_id", message.id)
            putExtra("sender_id", message.senderId)
        }
        val replyPendingIntent = PendingIntent.getBroadcast(
            this, message.id.hashCode(), replyIntent,
            PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT // Mutable needed for RemoteInput
        )

        // Mark as read action
        val markReadIntent = Intent(this, MarkReadReceiver::class.java).apply {
            putExtra("message_id", message.id)
        }
        val markReadPendingIntent = PendingIntent.getBroadcast(
            this, message.id.hashCode() + 1, markReadIntent, FLAG_IMMUTABLE
        )

        val replyAction = NotificationCompat.Action.Builder(
            R.drawable.ic_reply, "Reply", replyPendingIntent
        )
            .addRemoteInput(remoteInput)
            .setAllowGeneratedReplies(true)
            .build()

        val notification = NotificationCompat.Builder(this, MESSAGES_CHANNEL_ID)
            .setContentTitle(message.sender)
            .setContentText(message.text)
            .setSmallIcon(R.drawable.ic_message)
            .addAction(replyAction)
            .addAction(R.drawable.ic_check, "Mark Read", markReadPendingIntent)
            .build()

        notify(message.id, notification)
    }
}

// BroadcastReceiver for inline reply
class ReplyReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val replyText = RemoteInput.getResultsFromIntent(intent)?.getString("reply_key")
        val messageId = intent.getIntExtra("message_id", -1)

        if (replyText != null) {
            // Send reply via API
            CoroutineScope(Dispatchers.IO).launch {
                api.sendReply(messageId, replyText)
            }
            // Update notification to show sent confirmation
            val updatedNotification = NotificationCompat.Builder(context, MESSAGES_CHANNEL_ID)
                .setContentText("You: $replyText")
                .setSmallIcon(R.drawable.ic_message)
                .build()
            NotificationManagerCompat.from(context).notify(messageId, updatedNotification)
        }
    }
}

Output: Inline reply with UI for text input and confirmation.

Prevention

  • Use RemoteInput for inline replies (adds text input to notification).
  • Use PendingIntent.FLAG_MUTABLE for actions with RemoteInput.
  • Use BroadcastReceiver for background action handling.
  • Update notification after action to provide user feedback.

Common Mistakes with notification action

  1. Mixing let bindings with <- bindings in do notation, producing type errors
  2. Overlapping type class instances that cause GHC to reject the program with ambiguous dispatch errors
  3. Non-exhaustive pattern matches that compile with warnings then crash at runtime

These mistakes appear frequently in real-world Android code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.

Practice Exercise

Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.

This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.

FAQ

### How do I add multiple actions to a notification?

Use .addAction() up to 3 times. Each action needs a unique PendingIntent. Android may collapse some actions depending on the notification style.

### What is setAllowGeneratedReplies?

It enables Smart Reply suggestions from the Android System UI. The system generates reply suggestions based on the conversation context.

### Can I show a progress bar in a notification action?

Progress is not per-action. Use setProgress(max, progress, indeterminate) on the NotificationCompat.<a href="/design-patterns/builder/">Builder</a> itself for download/upload progress notifications.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro