Building a Reminder App with Local Notifications using WorkManager API
Published: 2022-02-26 20:52:40

## **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.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q8rn3yg6n6tkra3otj3j.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7q33d5771rd9rb6ibnk1.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/swdjbjfxf45xoxihm0in.png)

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.`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abbx6mgctn3dnyoi2r13.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q0bkfzouo99tez6u8jf3.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.

```kotlin
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.

```kotlin
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 the `MainActivity` 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.

```groovy
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.

```kotlin
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.

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
	}
}
```

Create a private function in the`MainActivity` 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.

```kotlin
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.

```kotlin
<?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.

```kotlin
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.

{% embed https://youtu.be/C7t1Wd1gLTw %}

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](https://github.com/Blazebrain/reminder_app).

## 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](https://twitter.com/Blazebrain01) or [LinkedIn](https://linkedin.com/in/david-adegoke), I would be glad to assist. Do have a pleasant day.