| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A learning project: a small Kotlin + Jetpack Compose reminder app. The UI is simple on purpose. The point is Android system design — how a reminder survives app close, process death, and device reboot.
Problem: How do we reliably schedule reminders that survive app closure, process death, and device reboot?
Answer in this app:
From the command line:
./gradlew :app:installDebugThen open ReminderApp on the device.
Saving a time in the past, or a blank title, shows an error and writes nothing.
On a PENDING card:
The row stays in the list so you can see history.
The old alarm is cancelled, Room is updated, then a new alarm is scheduled with the same id.
When the alarm fires you get a notification with the title plus:
Those update Room even if you never open the app.
Do these in order. They are the actual constraints this project is built around.
Create two reminders for the same minute. Both should notify. Each alarm uses requestCode = reminder.id.
Create a reminder, then tap Cancel before it fires. You should not get a notification. If the alarm was already being delivered, ReminderReceiver re-reads Room and skips non-PENDING rows.
Unit tests are JVM tests. They use fakes for Room and AlarmManager — no emulator required.
./gradlew :app:testDebugUnitTestIn Android Studio: right-click app/src/test → Run Tests.
| Test | What it proves |
|---|---|
| CreateReminderUseCaseTest | Persist then schedule; blank title; time in the past; two reminders at the same timestamp get distinct ids |
| DeleteReminderUseCaseTest | Status becomes CANCELLED and the alarm is cancelled |
| CompleteReminderUseCaseTest | Status becomes COMPLETED and the alarm is cancelled |
| UpdateReminderUseCaseTest | Room update then alarm replace; past time does not write |
| RescheduleRemindersUseCaseTest | After "reboot", only future PENDING rows are scheduled; same id is not duplicated |
ReminderScheduler is an interface so tests can use FakeReminderScheduler.
app/src/main/java/com/reminder/app/ ├── data/ Room + repository implementation ├── domain/ models, repository contract, use cases ├── scheduler/ ReminderScheduler + AlarmManager implementation ├── receiver/ alarm, boot, notification-action BroadcastReceivers ├── notification/ NotificationCompat + channel ├── presentation/ Compose screens + ViewModels (StateFlow) └── di/ Hilt modules
Compose UI
→ ViewModel (StateFlow)
→ Use case
→ Room (source of truth)
→ ReminderScheduler (AlarmManager) // after persist
→ ReminderReceiver
→ re-read Room
→ notification if still PENDING
MVVM + a thin use-case layer. No BaseViewModel. Hilt constructor injection.
Save
→ CreateReminderUseCase
1. Reject blank title / time in the past
2. repository.create(...) // Room INSERT, get id
3. scheduler.schedule(...) // AlarmManager
→ PendingIntent requestCode = reminder.id
FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE
→ at triggerTimeMillis
→ ReminderReceiver.goAsync()
load Room; if PENDING → show notification
Persist then schedule. If the process dies after insert, the next app start / reboot recovery schedules from Room.
BOOT_COMPLETED
→ BootReceiver
→ RescheduleRemindersUseCase
PENDING rows where triggerTimeMillis > now
scheduler.schedule(each)
The same use case runs in ReminderApplication.onCreate. That covers force-stop: Android wipes alarms and will not deliver BOOT_COMPLETED until the user opens the app.
Scheduling the same id twice replaces the previous alarm (no duplicates).
| Need | AlarmManager | Room |
|---|---|---|
| List reminders in the UI | Cannot query | Flow from DAO |
| Survive process death | Yes (OS-owned) | Yes (on disk) |
| Survive reboot | No | Yes |
| Know cancelled vs pending | No | ReminderStatus |
| Edit the time | Cancel + reschedule | Update row, then reschedule |
The UI never treats in-memory state as truth. ReminderListViewModel collects observeReminders(). After process death, a new process collects again.
When an alarm fires, ReminderReceiver re-reads Room. Cancelled/completed rows do not notify.
| Approach | Process death | Exact wall-clock | Reboot | Fit for reminders? |
|---|---|---|---|---|
| Coroutine delay | No | While alive | No | No |
| Handler | No | While alive | No | No |
| WorkManager | Yes | No (deferrable) | Yes | Poor |
| AlarmManager | Yes | Yes, with caveats | No (need BootReceiver) | Yes |
WorkManager is for deferrable work. A reminder is a wall-clock event. This app uses AlarmManager.RTC_WAKEUP.
This app never assumes exact alarms are available:
Android 14+ denies SCHEDULE_EXACT_ALARM by default. This project does not declare USE_EXACT_ALARM, so the fallback path stays real.
| Permission | Why |
|---|---|
| POST_NOTIFICATIONS | Android 13+ runtime permission. Without it, notify() throws. We catch that. |
| SCHEDULE_EXACT_ALARM | Android 12+ special app-op (not a normal runtime permission). |
| RECEIVE_BOOT_COMPLETED | Lets BootReceiver run after reboot. |
Receivers:
Force-stop: alarms are cleared and BOOT_COMPLETED will not run until the user opens the app. Reschedule-on-start handles this.
Reminder(
id: Long,
title: String,
triggerTimeMillis: Long,
status: ReminderStatus // PENDING | COMPLETED | CANCELLED
)| Back | FazBrowse Home | New Git URL |