Android Notification Action — Complete Guide
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
RemoteInputfor inline replies (adds text input to notification). - Use
PendingIntent.FLAG_MUTABLEfor actions withRemoteInput. - Use
BroadcastReceiverfor background action handling. - Update notification after action to provide user feedback.
Common Mistakes with notification action
- Mixing let bindings with <- bindings in do notation, producing type errors
- Overlapping type class instances that cause GHC to reject the program with ambiguous dispatch errors
- 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
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro