Scheduled local notification in Android using AlarmManager
In today’s post, we’re diving into how to set up scheduled local notifications using AlarmManager. It’s a cool trick to keep all your tasks in check, and I can’t wait to show you how it’s done.
Scheduled local notifications are a fantastic tool for enhancing user engagement and ensuring that users stay connected with apps even when they’re not actively using them. Here are a few examples that show where they’re helpful:
- Productivity Apps:
For productivity apps, scheduled notifications remind users about upcoming deadlines, meetings, or tasks. It’s like having a personal assistant that keeps you on track throughout your day, ensuring you never miss an important event or deadline. - Medication Reminders:
If your app involves managing medications, implementing scheduled notifications can be a game-changer. By setting up reminders for specific medication doses, users can ensure they never miss an important pill, maintaining the effectiveness of their treatment and promoting better health outcomes. - Study or Work Breaks:
Need to boost productivity and prevent burnout? Your app can provide users with the option to set notifications for study or work breaks. Reminding users to take regular breaks can improve focus, reduce stress, and ultimately enhance overall performance. - Health and Fitness Apps:
In health apps, reminders can prompt users to drink water, stand up and move after periods of inactivity, or take their medication at the right time. These timely nudges can significantly improve habits and overall well-being. - Educational Apps:
For those using educational apps, notifications can remind users to complete their daily learning goals or inform them about a new topic to explore. This keeps learning interactive and prevents users from losing interest. - Gaming:
Even games can use scheduled notifications to bring players back by reminding them about reward availability, special events, or just to continue playing to maintain a streak or status. - Bill Payments:
Late bill payments can be a hassle, leading to unnecessary fees and penalties. By integrating scheduled notifications for bill due dates, your app can help users stay on top of their finances and avoid missed payments. It’s like having a personal financial assistant right in their pocket. - Event Countdowns:
Everyone loves a good countdown, especially when it’s for a special event! If your app involves event planning or management, offering countdown notifications can add an extra layer of excitement. Whether it’s a birthday, anniversary, or important appointment, users will appreciate the timely reminders to ensure they’re always prepared and never miss out on the fun. - Habit Building:
Want to help users build and maintain healthy habits? Scheduled notifications are the way to go. Whether it’s reminding users to drink water, stretch their legs, or practice a new skill, your app can play a crucial role in supporting their journey towards a healthier lifestyle.
Getting Started: Implementing Local Scheduled Notifications
Okay, let’s jump into setting up local scheduled notifications. We’ll use AlarmManager to schedule our alarms, and for the user interface, I’ll be using Jetpack Compose. But hey, if you’re more comfortable with XML, no worries! This code will work just fine with XML layouts too. Ready to roll?
Overview of the Scheduled Local Notification Implementation
Here’s our game plan:
- Dependencies and Permissions:
We’ll start by setting up the project with the stuff we need. This means adding theaccompanist-permissions
andmaterial3:1.2.0-alpha12
libraries, and declaring a few permissions likePOST_NOTIFICATIONS
,SCHEDULE_EXACT_ALARM
,USE_EXACT_ALARM
in the AndroidManifest.xml file. - Notification Application Setup:
Next up, we have theScheduleNotificationApplication
class. This is where the magic happens. It creates a special notification channel, making it easy to organize and manage notifications like a pro. - Crafting Notification:
When it comes to making notifications, we’ve gotReminderNotification
class to help us out. It uses theNotificationCompat.Builder()
to create the exact type of notification we want, with all the bells and whistles. - Setting up AlarmManager:
In theScheduleNotification
class, we'll kick things off by setting up our trustyAlarmManager
and scheduling our notifications. This is where the real action happens! - Setting up Broadcast Receiver:
Can’t forget about the broadcast receiver! We’ll see how it fits into the picture and make sure to mention it in our manifest.
So, that’s the plan for now. Let’s roll up our sleeves, dive into this blueprint, and start coding!
Let’s Code
Starting with build.gradle.kts (:app)
—
implementation("androidx.compose.material3:material3:1.2.0-alpha12")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
implementation("com.google.accompanist:accompanist-permissions:0.31.1-alpha")
comopse.material3
— This dependency provides Material Design components specifically designed for Jetpack Compose. It includes various UI elements such as buttons, cards, dialogs, and more, all styled according to the Material Design guidelines.
accompanist
— These libraries streamlines the process, making it hassle-free to handle permissions. Now that we’ve added the required dependencies to our project, let’s sync the project to make sure everything is up-to-date.
Once the sync is complete, give our app a little permission boost! We need to add the few permissions in our AndroidManifest.xml
.
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
POST_NOTIFICATION
: Allows app to post notifications to the device.SCHEDULE_EXACT_ALARM
: Grants app permission to schedule precise alarms.USER_EXACT_ALARM
: Enables app to use precise alarm scheduling.
Now that we’ve got the dependencies and permissions sorted out, let’s shift our focus to the user interface. I’ve kept it simple: three text fields for the title, date, and time, along with a button to add a reminder. Take a look at the image below for reference.
Next up, let’s add the Date and Time picker. The Material-3 library has a Date picker dialog, but it’s missing a Time picker. No worries though, I found a TimePickerDialog compose function (I’ll credit the source later) that we’ll use. Let’s dive into the code!
TimePickerDialog.kt
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@Composable
fun TimePickerDialog(
title: String = "Select Time",
onDismissRequest: () -> Unit,
confirmButton: @Composable (() -> Unit),
dismissButton: @Composable (() -> Unit)? = null,
containerColor: Color = MaterialTheme.colorScheme.surface,
content: @Composable () -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
usePlatformDefaultWidth = false
),
) {
Surface(
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 6.dp,
modifier = Modifier
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.background(
shape = MaterialTheme.shapes.extraLarge,
color = containerColor
),
color = containerColor
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 20.dp),
text = title,
style = MaterialTheme.typography.labelMedium
)
content()
Row(
modifier = Modifier
.height(40.dp)
.fillMaxWidth()
) {
Spacer(modifier = Modifier.weight(1f))
dismissButton?.invoke()
confirmButton()
}
}
}
}
}
The TimePickerDialog will give us the time in hours and minutes. I believe it’s a good time to finish setting up our dialog box.
To simplify, I’ll manage the dialog box visibility and value retrieval within our AddScheduleScreen()
compose function, which represents the main screen UI. If this approach seems to complicate the code, feel free to adjust the structure based on your project’s needs.
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@Composable
fun AddScheduleScreen() {
val context = LocalContext.current
val date = remember { Calendar.getInstance().timeInMillis }
val formatter = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = date)
var showDatePicker by remember { mutableStateOf(false) }
val timePickerState = rememberTimePickerState()
var showTimePicker by remember { mutableStateOf(false) }
// ...
}
context
: Local context retrieved from the current Composable.date
: Current time in milliseconds.formatter
: Date format for display.datePickerState
: State for managing date picker.showDatePicker
: Visibility state for date picker.timePickerState
: State for managing time picker.showTimePicker
: Visibility state for time picker.
If you encounter errors with rememberDatePickerState()
and rememberDatePickerState()
, simply press Alt + Enter
and choose Otp in for ‘ExperimentalMaterial3Api` on AddScheduelScreen
.
import androidx.compose.material3.DatePicker
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.DatePickerDialog
import com.codebyzebru.schedulenotificationdemo.ui.baseutils.TimePickerDialog
import java.util.Calendar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddScheduleScreen() {
// ...
if (showDatePicker) {
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(
onClick = {
val selectedDate = Calendar.getInstance().apply {
timeInMillis = datePickerState.selectedDateMillis!!
}
scheduleDate = formatter.format(selectedDate.time)
showDatePicker = false
}
) { Text("OK") }
},
dismissButton = {
TextButton( onClick = { showDatePicker = false }
) { Text("Cancel") }
}
) { DatePicker(state = datePickerState) }
}
if (showTimePicker) {
TimePickerDialog(
onDismissRequest = { showTimePicker = false },
confirmButton = {
TextButton(
onClick = {
scheduleTime = "${timePickerState.hour}:${timePickerState.minute}"
showTimePicker = false
}
) { Text("OK") }
},
dismissButton = {
TextButton(
onClick = { showTimePicker = false }
) { Text("Cancel") }
}
) { TimePicker(state = timePickerState) }
}
// ...
}
If showDatePicker
is true, a DatePickerDialog is displayed. Upon confirmation, the selected date is formatted and stored in scheduleDate
, and the dialog is dismissed. Similarly, if showTimePicker
is true, a TimePickerDialog is shown. Upon confirmation, the selected time is formatted and stored in scheduleTime
, and the dialog is dismissed.
Now that we’ve got everything set up, it’s time to start building the notification.
Starting with notification channel, I like to create the notification channel in the application class because it ensures it’s set up properly for the entire lifespan of the app.
ScheduleNotificationApplication.kt
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import com.codebyzebru.schedulenotificationdemo.baseutils.AppConstants.NotificationKeys.RMNDR_NOTI_CHNNL_ID
import com.codebyzebru.schedulenotificationdemo.baseutils.AppConstants.NotificationKeys.RMNDR_NOTI_CHNNL_NAME
class ScheduleNotificationApplication: Application() {
@androidx.annotation.RequiresApi(Build.VERSION_CODES.O)
override fun onCreate() {
super.onCreate()
val notificationChannel = NotificationChannel(
RMNDR_NOTI_CHNNL_ID,
RMNDR_NOTI_CHNNL_NAME,
NotificationManager.IMPORTANCE_HIGH
)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(notificationChannel)
}
}
Notification channel setup complete. now let’s customize our notification to include essential elements like title, message, and large icon. I’ll keep it simple, but feel free to personalize it to suit your needs. If you’re interested in exploring different types of local notifications, I’ve written a blog on it — you can check it out for more ideas!
ReminderNotification.kt
import android.app.NotificationManager
import android.content.Context
import android.graphics.BitmapFactory
import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
import com.codebyzebru.schedulenotificationdemo.R
import com.codebyzebru.schedulenotificationdemo.baseutils.AppConstants.NotificationKeys.RMNDR_NOTI_CHNNL_ID
import com.codebyzebru.schedulenotificationdemo.baseutils.AppConstants.NotificationKeys.RMNDR_NOTI_ID
class ReminderNotification(private val context: Context) {
private val notificationManager = context.getSystemService(NotificationManager::class.java)
fun sendReminderNotification(title: String?) {
val notification = NotificationCompat.Builder(context, RMNDR_NOTI_CHNNL_ID)
.setContentText(context.getString(R.string.app_name))
.setContentTitle(title)
.setSmallIcon(R.drawable.round_notifications_active_24)
.setLargeIcon(BitmapFactory.decodeResource(context.resources,
R.drawable.round_notifications_active_24
))
.setPriority(NotificationManager.IMPORTANCE_HIGH)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText("It's time for $title")
)
.setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(context.bitmapFromResource(R.drawable.code_with_zebru))
)
.setAutoCancel(true)
.build()
notificationManager.notify(RMNDR_NOTI_ID, notification)
}
private fun Context.bitmapFromResource(
@DrawableRes resId: Int
) = BitmapFactory.decodeResource(
resources,
resId
)
}
Now that we’ve finished setting up the notification channel and building the notification, let’s shift our focus to the BroadcastReceiver. You might be wondering why we need it in this context.
So while using AlarmManager to trigger scheduled notifications, a BroadcastReceiver acts as the listener for the alarm events. When the AlarmManager fires off an alarm at the specified time, it sends out a broadcast. The BroadcastReceiver then intercepts this broadcast and triggers the necessary actions in response, such as showing a notification.
Essentially, the BroadcastReceiver serves as the bridge between the AlarmManager and your app’s logic for handling scheduled notifications. It ensures that your app can react appropriately when the scheduled time for a notification arrives, allowing you to deliver timely and relevant notifications to your users.
ReminderReceiver.kt
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.codebyzebru.schedulenotificationdemo.baseutils.AppConstants.NotificationKeys.RMNDR_NOTI_TITLE_KEY
import com.codebyzebru.schedulenotificationdemo.notification.ReminderNotification
class ReminderReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val scheduleNotificationService = context?.let { ReminderNotification(it) }
val title: String? = intent?.getStringExtra(RMNDR_NOTI_TITLE_KEY)
scheduleNotificationService?.sendReminderNotification(title)
}
}
Don’t forget to mention our BroadcastReceiver in AndroidManifest.xml
.
<application>
// ...
<receiver
android:name=".broadcast.ReminderReceiver"
android:enabled="true" />
</application>
android:enabled="true"
: This attribute indicates whether the receiver is enabled or disabled. Setting it to "true" means the BroadcastReceiver is enabled and can receive broadcast events, its default value will be considered as “true”. So, the BroadcastReceiver will be enabled by default and will be able to receive broadcast events.
However, it’s good practice to explicitly mention the android:enabled
attribute to make the code clearer and to avoid any confusion about the enabled/disabled state of the BroadcastReceiver.
Also, I forgot to mention our application class in the AndroidManifest, so let’s add that too.
<application
android:name=".ScheduleNotificationApplication"
// ...
</application>
We’ve reached the end; now let’s implement the AlarmManager.
ScheduleNotification.kt
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.compose.material3.DatePickerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TimePickerState
import com.codebyzebru.schedulenotificationdemo.baseutils.AppConstants.NotificationKeys.RMNDR_NOTI_ID
import com.codebyzebru.schedulenotificationdemo.baseutils.AppConstants.NotificationKeys.RMNDR_NOTI_TITLE_KEY
import com.codebyzebru.schedulenotificationdemo.receiver.ReminderReceiver
import java.util.Calendar
class ScheduleNotification {
@OptIn(ExperimentalMaterial3Api::class)
fun scheduleNotification(
context: Context,
timePickerState: TimePickerState,
datePickerState: DatePickerState,
title: String
) {
val intent = Intent(context.applicationContext, ReminderReceiver::class.java)
intent.putExtra(RMNDR_NOTI_TITLE_KEY, title)
val pendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
RMNDR_NOTI_ID,
intent,
PendingIntent.FLAG_MUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val selectedDate = Calendar.getInstance().apply {
timeInMillis = datePickerState.selectedDateMillis!!
}
val year = selectedDate.get(Calendar.YEAR)
val month = selectedDate.get(Calendar.MONTH)
val day = selectedDate.get(Calendar.DAY_OF_MONTH)
val calendar = Calendar.getInstance()
calendar.set(year, month, day, timePickerState.hour, timePickerState.minute)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Toast.makeText(context, "Reminder set!!", Toast.LENGTH_SHORT).show()
}
}
This scheduleNotification
function is responsible for scheduling a notification using the AlarmManager in an Android application. Let's break down what each part of the function does:
- We start by creating an
intent
, a messaging object representing an operation to be performed. This intent is directed to theReminderReceiver
class, indicating that it will handle the alarm when triggered. Additionally, we attach extra data to the intent, such as the title of the notification. - Using this intent, we create a PendingIntent using
PendingIntent.getBroadcast()
. PendingIntent is a token allowing other applications to perform operations on behalf of our application. This PendingIntent will be used to trigger the alarm broadcast. - We obtain an instance of the AlarmManager system service using
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
. The AlarmManager is responsible for delivering scheduled events (alarms) to the application at specified times. - After that, we retrieve the selected date from the
datePickerState
and extract the year, month, and day components. - We also initialize a
calendar
object with the current date and time. We set the calendar object to the selected date and time from the date picker and time picker states. - Finally, we use
alarmManager.setExactAndAllowWhileIdle()
to schedule the alarm. This method sets the alarm to trigger at the exact specified time, even if the device is in low-power idle modes. - We pass
AlarmManager.RTC_WAKEUP
to specify the type of alarm, the time in milliseconds when the alarm should go off, and the PendingIntent that will be triggered when the alarm fires.
Now, when the button is clicked, we’ll call the scheduleNotification function.
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.codebyzebru.schedulenotificationdemo.notification.ScheduleNotification
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddScheduleScreen() {
// ...
Button(
onClick = {
ScheduleNotification().scheduleNotification(context, timePickerState, datePickerState, scheduleText)
},
// ...
) { Text(text = "Add reminder") } // Button
// ...
}
And that’s it! To test the notification, simply enter a title, set the date and time, then click on the ‘Add reminder’ button. You’ll receive the notification at the scheduled time.
Wait a minute before you launch the app, you must need to implement notification permission handler, as you must be aware, from Android 13 we need to request for Push notification as it’s not granted by default so let’s handle Push notification request at runtime.
import android.Manifest
import android.os.Build
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@androidx.annotation.RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
fun AddScheduleScreen() {
// ...
val postNotificationPermission = rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
LaunchedEffect(key1 = true) {
if (!postNotificationPermission.status.isGranted) {
postNotificationPermission.launchPermissionRequest()
}
}
// ...
}
And that’s a wrap! With this implementation, we’ve added scheduled local notifications to our app. Utilize this feature across different scenarios we discussed earlier in the blog to grab user attention and boost engagement.
Now, launch the app on your device or emulator. If you’re on Android 13, you might see a permission dialog because, by default, it’s denied. Just grant it, and there you go! If you’re on Android 12 or an older version, no need to stress about notification permissions — it’s smooth sailing!
Worth your attention:
Local notification in Android with Jetpack compose (Importance and details implementation of local notification)
Different types of local notification in Android using Jetpack compose
I hope this article has provided valuable insights and assistance for your Android journey. Your support means a lot to me, so if you found this content helpful, please start following me and don’t hesitate to show some love with a hearty round of applause!👏
Your feedback fuels my passion for creating quality content. For any Android queries or just to connect, reach out on LinkedIn and Twitter.
Thanks for reading — looking forward to staying in touch!
Happy Coding!!