Introduction
Pause, tap your feet, scratch your hair a few times, squeeze your face a bit, then after a few choice seconds, voila, you remember, you finally yanked it off its hiding place. That piece of information, you eventually remember it! Sounds familiar? That’s what happens to many people daily across the entire continent. With the number of things we engage in, we tend to forget one or two pieces of information and then go through the scenario above, over and over. Well, mobile apps and technologies like Kotlin to the rescue. With the use of reminder applications, we can remind ourselves of essential/crucial events we must attend to; through notifications that pop up and draw our attention, we can adequately plan the day and be more productive.
In this article, we would build a Reminder app from scratch, cool yeah? You would learn to build a Reminder Application that uses Local Notifications to display reminders of events you have previously set. You would be able to set a message to display and select a Date and Time for the reminder.
Technologies to be used
As this tutorial will be hands-on, there are a few technologies that you must have and be familiar with.
- Kotlin Programming Language
- Android Studio
- Basic Layout in Android Development
- WorkManager API( we will discuss more of this in the next section).
What is WorkManager API?
WorkManager API is an API used to create scheduled notifications, i.e. pop-up messages that have been planned on the background process and delivered at a particular time. WorkManager makes it easy to schedule tasks to run even if the application exits or restarts. This means that our notifications would be delivered even if we closed the app. We would go through the step by step process of integrating the API into our app and using it to create a Reminder Application.
Let’s get practical
Project Setup
We’ll start by creating a new project in Android Studio. Select the Empty Activity Option, give the project a name of your choosing and then click ‘Finish’. Android Studio would create the basic structure, which we would build on as we proceed.
Now we are set to begin the actual coding; we would start from bottom to top, i.e., logic before layout or user interface.
The first thing we’ll do is create a folder named utils
; this folder would contain all the helper classes we would be making use of in the application.
Right-click on the package-name folder for your app and select New to create a new file. Select Kotlin Class/File and name it NotificationHelper
.
Building the NotificationHelper
The NotificationHelper class is the class that would handle all functionalities relating to notifications with the application.
We would pass in the context also as we would need it later on.
In this class, we’ll create two private variables
- A
channelId
to identify our notifications channel and - The
notificationId
to identify the notifications could be a random integer
We’ll be implementing the BigPicture NotificationStyle in our notifications, which means that our notifications would contain images and the reminder text. For this we would need two images. Grab two images from the internet or your local device. Drag and drop it into the drawable
folder of the res package. Import normally and also for v24. I have two images, checklist.png
and reminder_char.png.
Next, we will create a private function responsible for creating a notifications channel through which the NotificationManager would deliver our reminders. For that, we would need to check if the user's phone is Android version Oreo and above; this is because we don’t need to create a notification channel for older versions.
private fun createNotificationChannel(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT ).apply {
description = "Reminder Channel Description"
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
After checking, we create a notification channel using the channelID
we declared earlier. We also set the Notification Priority to default and set the description of our Notification channel. After this, we call context and access the NotificationService of the device and use it to create a Notifications Channel. With this, we’ve set up the function to create notification Channels.
Next, we create a function to createNotifications
. This makes it easy to reuse in other parts of our code.
fun createNotification(title: String, message: String){
// 1
createNotificationChannel()
// 2
val intent = Intent(context, MainActivity:: class.java).apply{
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
// 3
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
// 4
val icon = BitmapFactory.decodeResource(context.resources, R.drawable.reminder_char)
// 5
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.checklist)
.setLargeIcon(icon)
.setContentTitle(title)
.setContentText(message)
.setStyle(
NotificationCompat.BigPictureStyle().bigPicture(icon).bigLargeIcon(null)
)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build()
// 6
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notification)
}
1 - Call the createNotificationChannel
function created above.
2 - Create an Intent
that calls the MainActivity
when it runs. Adding an intent is to start up the app when the user clicks on the notification in the menu tray.
We also add flags to the intent:
-
Intent.FLAG_ACTIVITY_NEW_TASK
will ensure theMainActivity
opens up as a new Task on the history stack. It comes up as the root and appears as new in the history stack. -
Intent.FLAG_ACTIVITY_CLEAR_TASK
will cause any existing task associated with the activity to clear out before the activity starts.
3 - Since we are not launching the intent immediately, create a pending intent and pass the intent created to it. PendingIntent
in itself is a description of an Intent and what action it’s to perform. It gives an external application(like NotificationManager in our case)access to launch tasks for us as if we were launching them now, with the same set of permissions we would use. We call getActivity
, which means the PendingIntent is to start a newActivity when it launches.
4 - We go-ahead to create an icon for the notification using a BitmapFactory
. We have already imported the images we want to use earlier, so we call decodeResource
on BitmapFactory and then pass the image we want to use. We would use the icon in the notification.
5 - Next, we create the notification object by using NotificationCompat.Builder
and pass in the channelID. We go-ahead to set other configurations for our notifications like smallIcon
, largeIcon
, message(which will come from the message parameter goes into this function), the notificationStyle
(we’ll be using the BigPictureStyle
), we also pass in the icon we created in step 4. We also set the contentIntent
(the pendingIntent we created in step 3) and then the notificationPriority
(pass it as default). Lastly, we call build to add all these configurations as part of the object.
6 - We create the notification using the NotifcationManagerCompat
, passing in the NotificationID and the notification Object we created in Step 5.
With this, our NotificationHelper class is good to go, and we can start triggering “instant” notifications by simply calling the createNotifcation
function, passing in the title and description, and getting a notification.
However, this does not fulfill our aim as we want the notifications to pop up at a time we set, not instantly, i.e. we want a delayed notification. This is where WorkManager API comes in; using this API; we would be able to create a notification that would come up when we want.
Integrating the WorkManager API
To access the WorkManager API, we need to add it as a dependency. Open your build.gradle file in the GradleScripts section and add the work manager API there.
dependencies {
// other dependencies here
implementation "androidx.work:work-runtime-ktx:2.5.0"
}
Don’t forget to sync after adding this.
Next, we create a Worker
class named ReminderWorker
in the utils package. This class would inherit from the Worker class coming from the WorkManager API and give us access to a function doWork()
. This async function, which we will override in our class, would be responsible for calling the createNotification
function, defined in NotificationHelper. and creating the Notification. The ReminderWorker class takes in two parameters, the context
and the WorkerParameters
, which it would pass over to its Superclass.
Ok, let’s pause there and see how it looks in the code.
class ReminderWorker(val context: Context, val params: WorkerParameters) : Worker(context, params){
override fun doWork(): Result {
NotificationHelper(context).createNotification(
inputData.getString("title").toString(),
inputData.getString("message").toString())
return Result.success()
}
}
Since we want to be able to pass in our title and message for each Notification, we use the inputData
object, which is a key-value object that contains information that needs to be processed, in our case here, the title and object information that needs to be processed in the doWork
function. Finally, we return the result as a success.
Creating a WorkRequest
A WorkRequest is an object which contains all the information the WorkManager needs to schedule and run your work. It takes in a WorkerClass
that has defined the work to be done. There are two of WorkRequests:
- OneTimeWorkRequest: A WorkRequest which triggers only once.
- PeriodicWorkRequest: A WorkRequest which runs periodically based on set configuration.
Since we want our notifications to display only once, i.e. it should only remind us at the time and date we set, we would use the OneTimeWorkRequest.
To start with this, open up your MainActivity.kt
file; it contains an empty activity class with the contentView set to the activity_main.xml
file.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
Create a private function in theMainActivity
called createWorkRequest
. This function takes in a message, the note we want to add to the notifications. In this function, we create a WorkRequest object by calling the OneTimeRequestBuilder
and passing the ReminderWorker class.
We set configurations like initialDelay
. The OneTimeWorkRequest allows us to delay the time before the task(our notification) displays. We can set the delay to milliseconds, seconds, minutes, hours etc. For our reminder app, we want to hold our notification until the actual time and day we selected. To do that, we calculate the difference between the DateTime the user selected and the currentDateTime
. We would then pass this delay when setting the intialDelay
for the workRequest. We would go through each step while setting up the rest of the MainActivity
class.
We also set the InputData
through the workDataOf
object, passing a map of the values we want to use in our notifications. We would pass in the message through the message parameter for this function.
private fun createWorkRequest(message: String,timeDelayInSeconds: Long ) {
val myWorkRequest = OneTimeWorkRequestBuilder<ReminderWorker>()
.setInitialDelay(timeDelayInSeconds, TimeUnit.SECONDS)
.setInputData(workDataOf(
"title" to "Reminder",
"message" to message,
)
)
.build()
WorkManager.getInstance(this).enqueue(myWorkRequest)
}
Lastly, we call the WorkManager, get an instance and enqueue the request. With this, we are set! You’ve done well getting to this point. Now let’s go over and create a layout to link it up and see our Reminder App in its full beauty.
Laying out the View
We would set up a simple interface that would include:
- An EditText: to input the message you want to show in the notification
- A TimePicker: to select the time of the day you wish to show your notification
- A DatePicker: to choose the day you want the reminder to come up
- A Button: to set the notification with all the parameters specified.
Open up your res > layout > activity_main.xml file. We would use the Linear Layout and add each of those components listed above, giving them unique ids that would be used to reference them in the MainActivity
file.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ScrollView android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:gravity="center"
android:padding="10dp">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Message"/>
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:datePickerMode="spinner"
android:calendarViewShown="false"/>
<TimePicker
android:id="@+id/timePicker"
android:layout_marginTop="20dp"
android:layout_marginLeft="19dp"
android:timePickerMode="spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/setBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SET"/>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
Next, we head over to the MainActivity.kt
file and start making use of each of these components to set up our notifications.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1
var chosenYear = 0
var chosenMonth = 0
var chosenDay = 0
var chosenHour = 0
var chosenMin = 0
// 2
val descriptionText = findViewById<EditText>(R.id.editText)
val button = findViewById<Button>(R.id.setBtn)
val datePicker = findViewById<DatePicker>(R.id.datePicker)
val timePicker = findViewById<TimePicker>(R.id.timePicker)
val today = Calendar.getInstance()
// 3
datePicker.init(today.get(Calendar.YEAR), today.get(Calendar.MONTH),
today.get(Calendar.DAY_OF_MONTH)
) { _, year, month, day ->
chosenYear = year
chosenMonth = month
chosenDay = day
}
// 4
timePicker.setOnTimeChangedListener { _, hour, minute ->
chosenHour = hour
chosenMin = minute
}
// 5
button.setOnClickListener {
// 6 Get the DateTime the user selected
val userSelectedDateTime =Calendar.getInstance()
userSelectedDateTime.set(chosenYear, chosenMonth, chosenDay, chosenHour , chosenMin)
// 7 Next get DateTime for today
val todayDateTime = Calendar.getInstance()
// 8
val delayInSeconds = (userSelectedDateTime.timeInMillis/1000L) - (todayDateTime.timeInMillis/1000L)
// 9
createWorkRequest(descriptionText.text.toString(), delayInSeconds)
// 10
Toast.makeText(this, "Reminder set", Toast.LENGTH_SHORT).show()
}
}
1 - We create variables to hold the year, month, day, hour and minute that the user selects for the notification.
2 - We reference View components using their IDs, using the findViewById
function. We do this for each component, the EditText, the Button, the DatePicker and the TimePicker. We also get the current DateTime by getting an Instance of Calendar.
3 - We initialize the datePicker and assign the current year, month and day to it, so when the Calendar comes up, it starts from the current day and not well, from Feb 24, 1927. After initializing, we pass the values selected by the user to the variables we created in Step 1 for a year, month and day.
4 - We call setOnTimeChangeListener
on the TimePicker and then set the hour and minute the user selected to the variables we declared in Step 1 whenever the user interacts with the TimePicker.
5 - We call setOnClickListener
on the Button to run our logic when the user clicks the “Set” button in the View.
6 - Using the year, month, day, hour and minute that the user selected, we set an instance of Calendar. we save this instance to a variable
7 - We get an instance of Calendar to give us the current day DateTime.
8 - We convert the userSelectedDateTime
and todayDateTime
to milliseconds. After this, we divide them by 1000L, converting them to seconds. Finally, we subtract todayDateTime
from the userSelectedDateTime
to get the number of seconds the notification would delay before it displays to the user. We save the result to a variable, delayInSeconds
.
9 - We call the createWorkRequest
function and pass in the message from the descriptionText View component and the delayInSeconds
to it, creating the workRequest for the notification.
10 - We display a toast message informing the user that the Reminder is set.
With this done, let’s run our code and test it out.
And there you go, we have a full-fledged Reminder Application with Local Notification, built using the WorkManager API in Kotlin. You can checkout the source code for the app here.
Conclusion
Hurray! You got here; give yourself a round of applause; you deserve it! You’ve mastered displaying Local Notifications in An Android App using the WorkManager API. You’ve also built a simple app you can use personally to set reminders, cool yeah? Definitely! If you encounter any issue or want further clarifications, drop a comment below, reach out to me on Twitter or LinkedIn, I would be glad to assist. Do have a pleasant day.
Top comments (1)
Thank you. Very helpful