Table of Contents
Why Action Buttons Matter in Notifications
Action buttons let users respond directly from a notification. Instead of only tapping the notification to open your app, you can add separate buttons like "Reply", "Mark as read", or "Snooze". Each button can do a different task and can even run in the background without opening an Activity.
On Android, action buttons are part of the notification itself. You create them with the same NotificationCompat.Builder that you use for the main notification content, then attach a PendingIntent to each action. When the user taps a button, that PendingIntent is triggered.
Action buttons work together with notification channels and basic notification setup, but here you focus only on what is specific to buttons.
Basic Structure of a Notification Action
Every action button has three parts. It has an icon that is usually a small white outline, a label text that the user sees on the button, and a PendingIntent that Android will fire when the user taps that button.
You add an action to a notification by calling addAction on your NotificationCompat.Builder before you build the notification.
A simple example looks like this:
val intent = Intent(context, MyActionReceiver::class.java).apply {
action = "ACTION_MARK_AS_READ"
putExtra("MESSAGE_ID", messageId)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("New message")
.setContentText("You have a new message")
.addAction(
R.drawable.ic_mark_read,
"Mark as read",
pendingIntent
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
The key part for the action button is the addAction line. It defines the icon resource, the text on the button, and the PendingIntent that will be executed when the button is pressed.
Important rule: Every notification action must have a PendingIntent. Without it, the button will appear but do nothing.
Choosing Between Activity, Service, and Broadcast Actions
When you create the PendingIntent for an action button, you can decide what should respond. The three main options are an Activity, a Service, or a BroadcastReceiver.
Use a PendingIntent.getActivity when you want to open a screen in your app. For example, an action button "Open chat" might start a chat Activity directly, possibly with some extras to show a particular conversation.
Use a PendingIntent.getService when you want to start a Service that does background work. For example, "Download" could start a background download Service without showing any UI.
Use a PendingIntent.getBroadcast when you want a simple receiver that handles logic quickly and possibly updates the notification. This is very common for actions like "Dismiss", "Mark as read", or "Snooze".
Here is how those three variants look:
// 1. Start an Activity
val openIntent = Intent(context, DetailsActivity::class.java)
val openPendingIntent = PendingIntent.getActivity(
context,
0,
openIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 2. Start a Service
val serviceIntent = Intent(context, SyncService::class.java)
val servicePendingIntent = PendingIntent.getService(
context,
1,
serviceIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 3. Send a broadcast
val broadcastIntent = Intent(context, NotificationActionReceiver::class.java).apply {
action = "ACTION_DISMISS"
}
val broadcastPendingIntent = PendingIntent.getBroadcast(
context,
2,
broadcastIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)Choose the type based on whether you need UI, background work, or just lightweight handling.
Handling an Action in a BroadcastReceiver
Broadcast receivers are a very common way to react to action buttons. A receiver can read extras from the intent and then update your data or change the notification.
This is a simple BroadcastReceiver that handles a "Mark as read" action:
class NotificationActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
"ACTION_MARK_AS_READ" -> {
val messageId = intent.getStringExtra("MESSAGE_ID")
if (messageId != null) {
// Update message in storage or database
markMessageAsRead(messageId, context)
}
// Optionally cancel the notification
val notificationManager =
ContextCompat.getSystemService(
context,
NotificationManager::class.java
)
notificationManager?.cancel(NOTIFICATION_ID)
}
"ACTION_DISMISS" -> {
val notificationManager =
ContextCompat.getSystemService(
context,
NotificationManager::class.java
)
notificationManager?.cancel(NOTIFICATION_ID)
}
}
}
private fun markMessageAsRead(messageId: String, context: Context) {
// Implementation depends on how you store messages
}
}
You must also register this receiver. For simple static actions, you can declare it in the AndroidManifest.xml of your app, then reference the same action strings you use in your Intent.
Designing Useful and Clear Actions
Action buttons should be meaningful, limited in number, and clear to understand. Most notifications look best with one or two actions. Too many actions can make the notification confusing and can also cause layout issues on smaller screens.
Use short action labels that describe what will happen. Avoid generic labels like "OK" or "Do it". Use verbs such as "Reply", "Archive", "Snooze", or "Download". The icon should match the action so that experienced users can recognize it quickly.
Actions appear vertically on expanded notifications and some may be hidden if there is not enough space or if the notification is not expanded. On some devices, an action may only appear when the user swipes down to expand the notification.
Design rule: Each action button should perform a safe and reversible operation when possible. Destructive actions should be clear and may need a confirm step in your UI.
Custom Behavior for Dismiss and Cancel Actions
The main notification has its own dismiss behavior when the user swipes it away. Action buttons can also dismiss a notification on their own by calling cancel in your handling code.
If you want a dedicated "Dismiss" button, you can implement it as another action. The action handler should call NotificationManager.cancel with the same notification ID, or cancelAll if you want to hide multiple notifications at once.
Example of a "Dismiss" action:
val dismissIntent = Intent(context, NotificationActionReceiver::class.java).apply {
action = "ACTION_DISMISS"
putExtra("NOTIFICATION_ID", NOTIFICATION_ID)
}
val dismissPendingIntent = PendingIntent.getBroadcast(
context,
3,
dismissIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Reminder")
.setContentText("Meeting at 10:00")
.addAction(R.drawable.ic_close, "Dismiss", dismissPendingIntent)
The receiver can then read NOTIFICATION_ID and cancel exactly that notification.
Direct Reply Actions
Some notifications support direct text input, for example messaging apps that offer a "Reply" action. In such cases, the action does not just trigger a tap, it also carries the text typed by the user.
This feature uses RemoteInput. You create a RemoteInput object with a key to retrieve the text later, then attach it to your action.
Basic structure:
val remoteInput = RemoteInput.Builder("KEY_TEXT_REPLY")
.setLabel("Reply")
.build()
val replyIntent = Intent(context, ReplyReceiver::class.java).apply {
action = "ACTION_REPLY"
}
val replyPendingIntent = PendingIntent.getBroadcast(
context,
4,
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val replyAction = NotificationCompat.Action.Builder(
R.drawable.ic_reply,
"Reply",
replyPendingIntent
)
.addRemoteInput(remoteInput)
.build()
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("New message")
.setContentText("Hi, how are you?")
.addAction(replyAction)In your receiver, you must extract the reply text from the intent.
class ReplyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == "ACTION_REPLY") {
val replyText = RemoteInput.getResultsFromIntent(intent)
?.getCharSequence("KEY_TEXT_REPLY")
?.toString()
if (replyText != null) {
handleReply(replyText, context)
}
// Optionally update the notification to show that reply was sent
updateNotificationAfterReply(context)
}
}
private fun handleReply(replyText: String, context: Context) {
// Send reply to server or save it
}
private fun updateNotificationAfterReply(context: Context) {
// Build a new notification that says "Reply sent"
}
}Direct reply actions provide a powerful way to keep the user in the notification shade while still interacting with your app.
Behavior Across Android Versions
When you use NotificationCompat and PendingIntent consistently, basic actions work across many Android versions. However, some advanced features such as direct reply may behave differently or require specific minimum versions.
Modern Android versions prefer immutable PendingIntent flags for security, so you often combine PendingIntent.FLAG_UPDATE_CURRENT with PendingIntent.FLAG_IMMUTABLE. On older versions the immutable flag is ignored, but using it now helps your app follow current platform requirements.
Existing actions can also interact with notification channels that you define elsewhere. The channel controls importance and sound, but the action buttons still behave the same way once the user sees the notification.
Testing and Debugging Action Buttons
Testing notification actions requires you to pay attention to two parts. You must verify that the button appears correctly with the right icon and text, and you must verify that the PendingIntent actually triggers your target component.
Use Logcat to print messages from your BroadcastReceiver, Activity, or Service when an action is received. This helps you confirm that the intent action and extras are correct.
Multiple notifications that use similar actions should use request codes that avoid conflicts when needed. For example, if each notification represents a different message, you can use the message ID as the request code when you create the PendingIntent. This allows each action button to carry its own data and be handled independently.
With careful design and proper use of PendingIntent objects, action buttons can make your notifications much more interactive and useful without constantly taking users into your app UI.