Mastering Notifications in Android Using Jetpack Compose
By CodingBihar.com
In the ever-evolving world of Android development, Jetpack Compose has emerged as the modern toolkit for building native UI. But when it comes to system-level features like notifications, many developers still rely on XML-based approaches. So, the question is: Can we manage notifications effectively using Jetpack Compose? The answer is yes — and in this article, we’ll explore exactly how!
Whether you’re building a chat app, a music player, or a reminder tool, notifications play a crucial role in keeping users engaged. Let’s dive deep into how to implement and manage notifications the Compose way.
What Are Android Notifications?
Android notifications are messages shown to users outside your app's UI. They appear in the notification drawer, lock screen, or even as heads-up alerts.
You can use them to:
- Alert users about new messages or events
- Show progress (like downloads or uploads)
- Allow quick actions (like reply, mark as read, play/pause music)
Jetpack Compose and Notifications: Understanding the Basics
Jetpack Compose itself doesn’t provide direct APIs for notifications because they are part of the Android system (not UI). So, we still use the Notification APIs from the Android SDK, but we integrate them seamlessly within Compose-based apps.
Let’s look at a basic example step-by-step.
Step-by-Step: Creating a Simple Notification
1. Add Required Permissions (Optional)
No special permissions are needed for basic notifications, but for Android 13+ (API 33+), you must request POST_NOTIFICATIONS
permission at runtime.
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
2. Request Notification Permission (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
context as Activity,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
3. Create a Notification Channel (For Android 8.0+)
fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "General"
val descriptionText = "General Notifications"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel("channel_id", name, importance).apply {
description = descriptionText
}
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
Call this function once in MainActivity.
4. Build and Show the Notification
fun showNotification(context: Context, title: String, message: String) {
val builder = NotificationCompat.Builder(context, "channel_id")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
with(NotificationManagerCompat.from(context)) {
notify(1001, builder.build())
}
}
5. Trigger the Notification from Jetpack Compose UI
@Composable
fun NotificationScreen() {
val context = LocalContext.current
LaunchedEffect(Unit) {
createNotificationChannel(context)
}
Button(onClick = {
showNotification(context, "Hello from CodingBihar", "This is your first Jetpack Compose Notification!")
}) {
Text("Show Notification")
}
}
Full Coode
Manifest Notification Permission:
Loading...
MainActivity
package com.codingbihar.composenotificationdemo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.codingbihar.composenotificationdemo.ui.theme.ComposeNotificationDemoTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeNotificationDemoTheme {
MessageDemo()
}
}
}
}
Notification Screen
package com.codingbihar.composenotificationdemo
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@Composable
fun MessageDemo() {
val context = LocalContext.current
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted ->
val msg = if (isGranted) "Notification permission granted" else "Notification permission denied"
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
)
// Ask permission & create notification channel
LaunchedEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
showNotification(context)
}
Column (Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
){
Button(onClick = {
showNotification(context)
}
) {
Text("Show Notification")
}
}
}
fun showNotification(context: Context) {
val channelId = "my_channel_id"
// Create Notification Channel for Android 8+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val name = "Default Channel"
val descriptionText = "General Notifications"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(channelId, name, importance).apply {
description = descriptionText
}
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
if (ContextCompat.checkSelfPermission(
context,
android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
val largeIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_launcher_foreground)
// Build the notification
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.android) // Use your icon
.setLargeIcon(largeIcon)
.setContentTitle("Hello Compose")
.setContentText("This is a Jetpack Compose notification!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
with(NotificationManagerCompat.from(context)) {
notify(100, builder.build())
}
}
}
FINAL RESULT:
Advanced Notifications in Android using Jetpack Compose
Notifications are a crucial way to communicate with your app's users, and Jetpack Compose allows seamless integration with the Notification API. While most developers know how to show a simple notification, today we’ll take it to the next level by implementing advanced features like custom layouts, download progress, foreground service indicators, and scheduled reminders using WorkManager.
1. Custom Layouts Using RemoteViews
Want to build a notification that looks like a mini music player? Or show a stylized layout? You can use RemoteViews to create custom notification UIs using XML layouts.
Step 1: Create XML Layout (res/layout/custom_notification.xml)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:padding="10dp"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/albumArt"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_music" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Now Playing"
android:textStyle="bold"
android:paddingStart="10dp" />
</LinearLayout>
Step 2: Set RemoteView in Notification
val remoteView = RemoteViews(context.packageName, R.layout.custom_notification)
remoteView.setTextViewText(R.id.title, "Playing: Desi Beats")
val notification = NotificationCompat.Builder(context, "channel_id")
.setSmallIcon(R.drawable.ic_music_note)
.setCustomContentView(remoteView)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.build()
NotificationManagerCompat.from(context).notify(101, notification)
2. Foreground Service Notifications (e.g., for Music or Fitness)
When your app runs a long-running task like playing music or tracking steps, Android requires a foreground service with a visible notification.
Step 1: Create the Service
class MusicService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, "channel_id")
.setContentTitle("Music Playing")
.setContentText("Track: Chai & Code")
.setSmallIcon(R.drawable.ic_music_note)
.build()
startForeground(1, notification)
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
}
Step 2: Start Foreground Service
val serviceIntent = Intent(context, MusicService::class.java)
ContextCompat.startForegroundService(context, serviceIntent)
Don’t forget to add the FOREGROUND_SERVICE
permission in your manifest.
3. Progress Bars for Downloads
Let users track download or upload progress with dynamic progress bar notifications.
Example: Show Download Progress
val builder = NotificationCompat.Builder(context, "channel_id")
.setContentTitle("Downloading file")
.setSmallIcon(R.drawable.ic_download)
.setPriority(NotificationCompat.PRIORITY_LOW)
Thread {
for (progress in 0..100 step 10) {
builder.setProgress(100, progress, false)
NotificationManagerCompat.from(context).notify(102, builder.build())
Thread.sleep(500)
}
builder.setContentText("Download complete")
.setProgress(0, 0, false)
NotificationManagerCompat.from(context).notify(102, builder.build())
}.start()
This is perfect for file downloads, uploads, or any task with measurable completion.
4. Scheduled or Delayed Notifications using WorkManager
Want to remind users after 10 minutes or send daily quotes? WorkManager is perfect for background scheduling, even when the app is closed.
Step 1: Add Dependency
implementation "androidx.work:work-runtime-ktx:2.9.0"
Step 2: Create a Worker
class ReminderWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val notification = NotificationCompat.Builder(applicationContext, "channel_id")
.setContentTitle("Daily Tip")
.setContentText("Jetpack Compose is awesome! - CodingBihar")
.setSmallIcon(R.drawable.ic_reminder)
.build()
NotificationManagerCompat.from(applicationContext).notify(103, notification)
return Result.success()
}
}
Step 3: Schedule the Work
val request = OneTimeWorkRequestBuilder<ReminderWorker>()
.setInitialDelay(10, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context).enqueue(request)
You can also use PeriodicWorkRequestBuilder
for recurring tasks like daily reminders or motivational quotes.
Conclusion
Advanced notifications help make your Android app more professional and user-friendly. Here’s a quick summary:
- Custom Layouts: Use
RemoteViews
to show interactive and branded designs. - Foreground Services: Required for long-running tasks like music or fitness.
- Progress Indicators: Show download/upload progress in real-time.
- WorkManager: Schedule reminders, alarms, or updates even when the app is closed.
With Jetpack Compose and modern Android APIs, building powerful notification systems is easier than ever.
Custom Layout Notification:
package com.codingbihar.composenotificationdemo
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.widget.RemoteViews
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@Composable
fun MessageDemo() {
val context = LocalContext.current
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted ->
val msg = if (isGranted) "Notification permission granted" else "Notification permission denied"
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
)
// Ask permission & create notification channel
LaunchedEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
Column (Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
){
Button(onClick = {
NotificationHelper.showCustomNotification(context)
}) {
Text("Show Custom Notification")
}
}
}
object NotificationHelper {
private const val CHANNEL_ID = "custom_channel"
private const val CHANNEL_NAME = "Custom Notification"
fun showCustomNotification(context: Context) {
createNotificationChannel(context)
val customView = RemoteViews(context.packageName,
R.layout.custom_notification).apply { setTextViewText(R.id.notification_title, "Hello User!")
setTextViewText(R.id.notification_message, "This is a custom notification.")
setImageViewResource(R.id.notification_image, R.drawable.ic_launcher_foreground)
}
// Intent to open MainActivity when notification clicked
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
if (ContextCompat.checkSelfPermission(
context,
android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setCustomContentView(customView)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.notify(1001, notification)
}
}
private fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance)
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
}
res/layout/custom_notification.xml:
Loading...
Foreground Services:
Loading...
Progress Indicators:
Loading..
WorkManager:
Loading...