Let's create notification reminder app in Jetpack Compose.
Tags: kotlin, android, tutorial
Published: 2023-05-14 18:52:54

Get yourself a cup of coffee before we dive into this interesting tutorial.

```Kotlin
@Composable
fun Coffee(enough: Int, caffeine: Int) {
    when (enough) {
        in (caffeine + 1) downTo caffeine -> {
            drinkCoffee()
        }
        else -> {
            drinkCoffee()
        }
    }
}
```

**What we will cover.**
- Applying CI/CD Using GitHub Actions for Android.
- Creating notification reminders using compose.
- Schedule notification using Work Manager.

**Nice to have.**
- Prior knowledge coding in Kotlin.
- Familiarity with Android Studio tools & usage.
- Skills in Version Control (Git).
- Basic understanding of [Jetpack Compose](https://developer.android.com/jetpack/compose).
- [Scheduling tasks with WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)

## Step 1: Project setup.
- Open your Android Studio and tap `Create Project` which will take you to Templates wizard.

> In my case I am using...
> ```
> Android Studio Electric Eel | 2022.1.1 Patch 2
> Current Desktop: ubuntu:GNOME
> ```

- We will use _Empty Compose Activity (Material3)_ template which will generate for us a starter project.
![Templates wizard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8dxfxnxmsyd1i6uttwui.png)

- Click `Next` and in this stage we will give our project a nice name. In this app we will set our Minimum SDK to API level 23 which is equivalent to Android Version 6.0(Marshmallow). If you want to learn more about which Minimum API Level to choose depending on your app needs you can tap on `Help me choose` link below the Minimum SDK drop-down.
![Project Name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4qimgdy81bkyvd29vig.png)

- Once you are done you can click `Finish` and your project will build and generate our starter code.

> If you run into trouble with Gradle build, you can access `File > settings` of your project and manually update Gradle JDK version by pointing to the right local directory (I'm using version 11.0.15 at the time of this tutorial)

## Step 2: Setup GitHub Repository.

> If you're following this tutorial it's nice to have a GitHub account to complete this section. [If you don't have one, you can do that here.](https://docs.github.com/en/get-started/signing-up-for-github/signing-up-for-a-new-github-account)

- To easily complete this section you can connect your Android Studio to your GitHub account from `File > Settings > Version Control > GitHub`. If you're already connected we can share our project by accessing `VSC` menu on the top-bar menu and click `Share Project on GitHub`. Follow the dialog prompts, add, commit, and push your initial code (Android Studio will automatically create the repository for you).

![VCS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hoi7uu2bk4msayf698w3.png)

## Step 3: Configuring CI/CD Using GitHub Actions.
- Go to your [GitHub repo created above](https://github.com/RocqJones/ReminderApp) and click `Actions`.
- Search for `Android CI` and from results as shown below click `Configure`. Follow prompts and either commit directly to your main branch or checkout to a new branch which is a better practice. Do not edit `android.yml` file generated (For now we will go with the default generated configurations).

![CI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u2halbnzerup6m97gvqm.png)

> GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. 

**Setup Branch protection rules** 
From your repo settings got to `Branches` and create some rules. For `Branch name pattern` write the name of your default branch and check the following rules:
- Require a pull request before merging.
- Require status checks to pass before merging.
- Require branches to be up to date before merging.
- Do not allow bypassing the above settings.

Example output.
![CI output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/au5nnoueg153hnahoahl.png)



## Step 4: Let's get coding.

![Coding meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/plslsluzpsio6panp9km.jpg)

- We will start by creating some animated collapsible cards with some random data. Since this is not the main area of focus we will keep it simple.
- From our generated code we replace the `Greeting("Android")` method inside `onCreate` with `ListItems()`. Your `onCreate` should now look like:
```Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ReminderAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ListItems()
                }
            }
        }
    }
```

- In our `ListItems()` function we will create a list of 30 cards. To optimize our program's performance, we will use a lazy list instead of a `for-loop`. Lazy lists allow us to efficiently process large datasets by evaluating only the elements that are needed, on demand. This reduces memory consumption and speeds up our program's execution time, especially when working with large datasets.
```Kotlin
@Composable
fun ListItems(
    modifier: Modifier = Modifier,
    names: List<String> = List(30) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { n ->
            ComposeCard(name = n)
        }
    }
}
```

> Modularity and Reusability: We will break down the program's logic into different functions to improve the code's readability, maintainability, ease testing, and performance on multi-core processors.

- `ComposeCard()` will define our Composable card and it's content.
```Kotlin
@Composable
fun ComposeCard(name: String) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}
```
- Since we want to handle animations within our card, we will create other functions (`CardContent()`) that handle the card content and compose its state. Additionally, by isolating the animation logic in its own function, we can optimize its performance and ensure smooth, responsive animations.
```Kotlin
@Composable
fun CardContent(name: String) {
    val expanded = remember { mutableStateOf(false) }
}
```

> We use the **remember** function in Android Jetpack Compose to store and manage state within a composable function. This function creates a **MutableState** object instance, which we can use to store and update state values. By initializing the **expanded** variable using **remember**, we can update its value within our composable function's scope, and any changes will trigger recompositions of the function.

- Let's add a spring-based animation to our card make it feel more natural and engaging when clicked.
```Kotlin
Row(modifier = Modifier
       .padding(12.dp)
       .animateContentSize(
            animationSpec = spring(
               dampingRatio = Spring.DampingRatioMediumBouncy,
               stiffness = Spring.StiffnessLow
            )
       )
    ) { //... }
```
- To add content to our animated card, we can include a single column layout and an icon button with an **onClick** listener that handles the expanded state, as explained above.
```Kotlin
Column(modifier = Modifier.weight(1f).padding(12.dp)) {
    Text(text = "Hello")
    Text(text = "$name.",
        style = MaterialTheme.typography.headlineMedium.copy(
            fontWeight = FontWeight.ExtraBold
        )
    )

    if (expanded.value) {
        // Some random text here.
        Text(
            text = ("Jetpack Compose is a modern UI toolkit designed to simplify UI development.").repeat(2)
        )
    }
}

IconButton(onClick = { expanded.value = !expanded.value }) {
    Icon(
        painter = if (expanded.value) painterResource(id = R.drawable.baseline_expand_less_24) else painterResource(id = R.drawable.baseline_expand_more_24),
        contentDescription = if (expanded.value) {
            stringResource(R.string.show_less)
        } else {
            stringResource(R.string.show_more)
        }
    )
}
```

- At this point we already have a working collapsible list of items. We can override a dark theme on our app as follows:
```Kotlin
@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    name = "DefaultPreviewDark"
)
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun DefaultPreview() {
    ReminderAppTheme {
        ListItems()
    }
}
```

### Notifications in Compose using Work Manager.
In order to achieve our goal, we'll be utilizing `Android Jetpack WorkManager`. This powerful framework handles various types of persistent work, such as _Immediate_, _Long Running_, and _Deferrable_ tasks. For the purposes of this post, we'll be focusing on Immediate tasks.

- Using `WorkManager` offers numerous benefits to developers. Firstly, it ensures that background tasks are reliably executed, even if the app is closed or the device is rebooted. Secondly, it provides a flexible API for scheduling work that can be tailored to specific app requirements. Thirdly, it optimizes battery usage by intelligently deferring work until system resources become available, ensuring that the app does not consume excessive power. Furthermore, it supports chaining of tasks, which allows for the creation of complex workflows with minimal overhead. Lastly, it simplifies the management of scheduled tasks by providing a single, centralized location for monitoring and controlling their execution.

- Modify our `build.gradle(:app)` dependencies tree to have the following.
```gradle
dependencies {

    implementation 'androidx.core:core-ktx:1.10.1'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
    implementation 'androidx.activity:activity-compose:1.7.1'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.compose.material3:material3:1.0.1'

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"

    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"

    // work manager
    implementation("androidx.work:work-runtime-ktx:$work_version")

    // coroutines
    api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$rootProject.lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion"
    // Lifecycle utilities for Compose
    implementation "androidx.lifecycle:lifecycle-runtime-compose:$rootProject.lifecycleVersion"

    implementation 'androidx.fragment:fragment-ktx:1.5.7'

    implementation 'androidx.compose.material:material:1.4.3'
}
```
- For plugins have:
```gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}
```

- Modify our `build.gradle(Project:)` as follows
```gradle
buildscript {
    ext {
        compose_version = '1.4.3'
        work_version = "2.8.1"
        lifecycleVersion = '2.6.1'
        coroutines = '1.6.4'
    }
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.4.2' apply false
    id 'com.android.library' version '7.4.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
}
```

> If you're experiencing build issues related to compatibility issues, check out this [Compose to Kotlin Compatibility documentation](https://developer.android.com/jetpack/androidx/releases/compose-kotlin) and [Compose Compiler Stable Version](https://developer.android.com/jetpack/androidx/releases/compose-compiler). At the time of writing this post the above should work.

- To schedule reminders we're going to make some changes to our `MainActivity.kt` initially we we're using random data for our collapsible list. We're going to replace the list of 30 items we generate with real data from our local data sources. Create an `object DataSource { }` which holds `listOf(    ComposeRandomItem(//...))`.
- The `ComposeRandomItem()` data class structure.

```Kotlin
data class ComposeRandomItem(
    val name: String,
    val schedule: String,
    val type: String,
    val description: String
)
```

> For this section, [I prepared the `DataSource` which can be found here]()

- Replace `names` list from `fun ListItems` with `data: List<ComposeRandomItem> = DataSource.plants.map { it }`. The updated function should be as follows.
```kotlin
@Composable
fun ListItems(
    modifier: Modifier = Modifier,
    data: List<ComposeRandomItem> = DataSource.plants.map { it }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = data.toMutableList()) { n ->
            ComposeCard(
                name = n.name,
                type = n.type,
                description = n.description
            )
        }
    }
}
```
- Let's modify our `ComposeCard` parameters
```kotlin
@Composable
fun ComposeCard(name: String, type: String, description: String) { }
```

#### Custom reminder dialog.
Our custom `ReminderDialog` composable function takes two parameters: name, a string that represents the reminder's name, and onDismiss, a function that's invoked when the dialog is dismissed.
```Kotlin
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ReminderDialog(name: String, onDismiss: () -> Unit) {

    val schedules = listOf(
        R.string.schedule_5_seconds to 5000L,
        R.string.schedule_8_minutes to 8 * 60 * 1000L,
        R.string.schedule_1_day to 24 * 60 * 60 * 1000L,
        R.string.schedule_1_week to 7 * 24 * 60 * 60 * 1000L
    )

    Dialog(
        onDismissRequest = onDismiss,
        properties = DialogProperties(
            dismissOnBackPress = true,
            dismissOnClickOutside = true
        )
    ) {
        Surface(
            shape = RoundedCornerShape(16.dp),
            modifier = Modifier.padding(16.dp)
        ) {
            Column(modifier = Modifier.fillMaxWidth()) {
                Text(
                    text = stringResource(R.string.title_reminder),
                    fontWeight = FontWeight.Bold,
                    fontSize = 20.sp,
                    textAlign = TextAlign.Center,
                    modifier = Modifier.padding(vertical = 16.dp).fillMaxWidth()
                )

                schedules.forEach { (scheduleTextId, delayMillis) ->
                    ListItem(
                        text = { Text(text = stringResource(scheduleTextId)) },
                        modifier = Modifier.clickable {
                            // event
                            onDismiss()
                        }
                    )
                }
            }
        }
    }
}
```
- `@OptIn(ExperimentalMaterialApi::class)` is an annotation used in Kotlin to indicate that the annotated element is using experimental Material Design components or APIs that are subject to change in future versions.

- The string resources used.
```xml
<string name="title_reminder">Remind me in…</string>
<string name="channel_name">reminder_channel</string>
<string name="channel_description">reminder_reminder</string>
<string name="schedule_5_seconds">5 seconds</string>
<string name="schedule_8_minutes">8 minutes</string>
<string name="schedule_1_day">1 day</string>
<string name="schedule_1_week">1 week</string>
```

- To demonstrate the use of state in managing dynamic UI elements within our composable function, we will modify our `ComposeCard()` function as follows:
```kotlin
@Composable
fun ComposeCard(name: String, type: String, description: String) {
    val dialogState = remember { mutableStateOf(false) }

    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
        onClick = { dialogState.value = true }
    ) {
        CardContent(name, type, description)
    }

    if (dialogState.value) {
        ReminderDialog(name = name, onDismiss = { dialogState.value = false })
    }
}
```
- In this function, `dialogState` is a piece of state that is used to track whether the `ReminderDialog` component should be displayed or not. The state is updated in response to a click event on the `Card` component, and the dialog is conditionally displayed based on the state of `dialogState`.

#### Scheduling reminders using `WorkManager`.
- By using the _Android Jetpack WorkManager API_, we can schedule a one-time work request to display a reminder. This is achieved through the `ReminderViewModel` class which extends the `ViewModel` class and provides a function `scheduleReminder()`. This function takes in a duration, unit (TimeUnit), and plant name as input parameters. It will create a `OneTimeWorkRequest` with the `ReminderWorker` class as the work to be done, sets the input data for the work request using the plant name and description obtained from a list of items, and schedules the work request using the WorkManager instance. The `ReminderViewModelFactory` is a factory class that creates instances of the `ReminderViewModel` class. This approach allows for separation of concerns, making it easier to manage dependencies and testability in the application.
```kotlin
class ReminderViewModel(application: Application): ViewModel() {

    private val itemsList = DataSource.plants
    private val workManager = WorkManager.getInstance(application)

    internal fun scheduleReminder(
        duration: Long,
        unit: TimeUnit,
        plantName: String
    ) {
        // create a Data instance with the plantName passed to it
        val myWorkRequestBuilder = OneTimeWorkRequestBuilder<ReminderWorker>()
        for (items in itemsList.toMutableList()) {
            if (items.name == plantName) {
                myWorkRequestBuilder.setInputData(
                    workDataOf(
                        "NAME" to items.name,
                        "MESSAGE" to items.description
                    )
                )
            }
        }
        myWorkRequestBuilder.setInitialDelay(duration, unit)
        workManager.enqueue(myWorkRequestBuilder.build())
    }
}

class ReminderViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(ReminderViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            ReminderViewModel(application) as T
        } else {
            throw IllegalArgumentException("Unknown ViewModel class")
        }
    }
}
```
- Before creating our `ReminderWorker`, we will create our `BaseApplication` class which is a custom implementation of the `Application` class that we will used to create and register a notification channel for displaying reminders. The `onCreate()` method is overridden to create a notification channel with the specified name, description, and importance level. The `NotificationManager.IMPORTANCE_DEFAULT` indicates that the notifications from this channel will have medium importance and will make a sound. The channel is registered with the system by calling the `createNotificationChannel()` method of the `NotificationManager` class. The `CHANNEL_ID` constant is used to uniquely identify the notification channel and is made available through the companion object of the class.
```kotlin
class BaseApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = getString(R.string.channel_name)
            val descriptionText = getString(R.string.channel_description)
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
                description = descriptionText
            }
            // Register the channel with the system
            val notificationManager: NotificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }

    companion object {
        const val CHANNEL_ID = "reminder_id"
    }
}
```
Manifest.
```xml
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />

<application
        android:name=".base.BaseApplication">
</application>
```

- We will build our `ReminderWorker` class, which extends the `Worker` class and overrides the `doWork` method responsible for creating and displaying notifications to the user. The notification content will include the name of the plant and a reminder message. It will also set up a pending intent to launch the app's `MainActivity` when the user clicks on the notification. 
- The `BaseApplication.CHANNEL_ID` constant is used to identify the notification channel, and the `notificationId` field will assign a unique ID number to each notification. Finally, the notification will be displayed to the user by calling the `notify` method from `NotificationManagerCompat`.
```kotlin
class ReminderWorker(
    context: Context,
    workerParams: WorkerParameters
) : Worker(context, workerParams) {

    // Arbitrary id number
    private val notificationId = 17

    @SuppressLint("MissingPermission")
    override fun doWork(): Result {
        val intent = Intent(applicationContext, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }

        val pendingIntent: PendingIntent = PendingIntent.getActivity(
            applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE
        )

        val plantName = inputData.getString(nameKey)

        val body = "Hello, It's time to water your $plantName and spray pesticides to avoid powdery mildew."
        val builder = NotificationCompat.Builder(applicationContext, BaseApplication.CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_android_black_24dp)
            .setContentTitle("Reminder App.")
            .setContentText(body)
            .setStyle(NotificationCompat.BigTextStyle().bigText(body))
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)

        with(NotificationManagerCompat.from(applicationContext)) {
            notify(notificationId, builder.build())
        }

        return Result.success()
    }

    companion object {
        const val nameKey = "NAME"
    }
}
```

- To link our `ReminderViewModel` with our `ReminderDialog`, we use the `viewModel` function to create an instance of `ReminderViewModel`. We also pass an instance of `ReminderViewModelFactory` to create the view model. Then, we set up a `clickable` modifier on the composable element, which calls the `scheduleReminder` method on the `viewModel` instance when the user clicks on a specific item in the dialog. The `scheduleReminder` method takes the delay time, time unit, and name of the plant as its parameters, and uses these to create a work request to send a notification to the system. Finally, the `onDismiss` callback is called to dismiss the dialog.
```kotlin
val viewModel: ReminderViewModel = viewModel(
        factory = ReminderViewModelFactory(
            LocalContext.current.applicationContext as Application
        )
    )
//... Our previous code...

modifier = Modifier.clickable {
    viewModel.scheduleReminder(delayMillis, TimeUnit.MILLISECONDS, name)
    onDismiss()
}
```

## Conclusion
In this blog, we have discussed the process of creating a reminder app in Android using Kotlin and Jetpack. We started by setting up the basic UI of the app and then implemented the ViewModel and Repository classes to manage the app's data.

Next, we explored the use of WorkManager to schedule notifications for each plant in the app, and created a ReminderWorker class to handle the creation and display of notifications.

Throughout this process, we emphasized the importance of writing clean and maintainable code, and used best practices such as using dependency injection and following the single responsibility principle.

By following these steps, we were able to create a functional reminder app that can help users keep track of their plant care routines.

### References.
- [The complete source code of this series is located in this GitHub Repository](https://github.com/RocqJones/ReminderApp). You can fork to have your copy and create some more cooler features based on what we have learned.
- [Jetpack Compose documentation.](https://developer.android.com/jetpack/compose/documentation)
- [WorkManager.](https://developer.android.com/topic/libraries/architecture/workmanager)
- [Notifications.](https://developer.android.com/develop/ui/views/notifications)
- [ViewModel overview.](https://developer.android.com/topic/libraries/architecture/viewmodel)
- [Compose Material 3.](https://developer.android.com/jetpack/androidx/releases/compose-material3)