AlarmManager vs WorkManager
You could schedule daily repeating alarm at specific time with AlarmManager, but WorkManager does offer a few extra benefit.
- Ability to set constraints: like only run when network is available, or don't run when battery is low.
- Ensures task execution, even if the app or device restarts. Refer this.
- Support for retry policy, e.g. retry later if network error.
So, as a guideline, WorkManager is intended for tasks that require a guarantee that the system will run them, even if the app exits. It is not intended for background work that requires immediate execution or requires execution at an exact time. If you need your work to execute at an exact time (such as an alarm clock, or event reminder), use AlarmManager. For work that needs to be executed immediately but is long running, you’ll often need to make sure that work is executed when in the foreground; whether that’s by limiting execution to the foreground (in which case the work is not longer true background work) or using a Foreground Service. - Source
For my scenario, I need the reminder to show up around specific time, doesn't need to be exact.
Daily Repeating Task at Specific Time with WorkManager
You might be thinking of using PeriodicWorkRequest, but it is not suited to run task at specific time.
- It may run immediately, at the end of the period, or any time in between so long as the other conditions are satisfied at the time - Source. If you setup for a period of one day, it might run at the start of the day or the end of the day, with no gurantee of the time.
- Another problem is time drift. On Day 1 it might run at 6:00am, then subsequent days it might run on time or later, and it will get later and later as the days goes by (due to the doze and battery saving mechanism). By Day 10, it could be running at 10:00am.
We shall be using OneTimeWorkRequest.Builder, delay excution using setInitialDelay
based on the time. After the work for today is executed, we schedule the work for next day. To avoid accidentally setting up multiple alarm/request, we shall utiliaze enqueueUniqueWork.
Dependencies.
depedencies {
// https://mvnrepository.com/artifact/androidx.work/work-runtime-ktx
def work_version = '2.0.1'
// use -ktx for Kotlin+Coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
}
Code
class ReminderWorker(appContext: Context, workerParams: WorkerParameters): CoroutineWorker(appContext, workerParams) { companion object { private const val REMINDER_WORK_NAME = "reminder" private const val PARAM_NAME = "name" // optional - send parameter to worker // private const val RESULT_ID = "id" fun runAt() { val workManager = WorkManager.getInstance() // trigger at 8:30am val alarmTime = LocalTime.of(8, 30) var now = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES) val nowTime = now.toLocalTime() // if same time, schedule for next day as well // if today's time had passed, schedule for next day if (nowTime == alarmTime || nowTime.isAfter(alarmTime)) { now = now.plusDays(1) } now = now.withHour(alarmTime.hour).withMinute(alarmTime.minute) // .withSecond(alarmTime.second).withNano(alarmTime.nano) val duration = Duration.between(LocalDateTime.now(), now) Timber.d("runAt=${duration.seconds}s") // optional constraints /* val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() */ // optional data val data = workDataOf(PARAM_NAME to "Timer 01") val workRequest = OneTimeWorkRequestBuilder<ReminderWorker>() .setInitialDelay(duration.seconds, TimeUnit.SECONDS) // .setConstraints(constraints) .setInputData(data) // optional .build() workManager.enqueueUniqueWork(REMINDER_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) } fun cancel() { Timber.d("cancel") val workManager = WorkManager.getInstance() workManager.cancelUniqueWork(REMINDER_WORK_NAME) } } override suspend fun doWork(): Result = coroutineScope { val worker = this@ReminderWorker val context = applicationContext val name = inputData.getString(PARAM_NAME) Timber.d("doWork=$name") var isScheduleNext = true try { // do something // possible to return result // val data = workDataOf(RESULT_ID to 1) // Result.success(data) Result.success() } catch (e: Exception) { // only retry 3 times if (runAttemptCount > 3) { Timber.d("runAttemptCount=$runAttemptCount, stop retry") return@coroutineScope Result.success() } // retry if network failure, else considered failed when(e.cause) { is SocketException -> { Timber.e(e.toString(), e.message) isScheduleNext = false Result.retry() } else -> { Timber.e(e) Result.failure() } } } finally { // only schedule next day if not retry, else it will overwrite the retry attempt // - because we use uniqueName with ExistingWorkPolicy.REPLACE if (isScheduleNext) { runAt() // schedule for next day } } }}
NOTE: CoroutineWorker is used the example above.
Usage
ReminderWorker.runAt()
References: