Android development / Advanced: reliable mobile systems and releases / Lesson 14
Durable background work and scheduling
Select a background mechanism by urgency, visibility, and durability while respecting platform scheduling limits.
On this page
Lesson goals
- Use WorkManager for eligible persistent deferrable work.
- Design idempotent workers and classify retryable failures.
Classify the work before choosing an API
A screen refresh belongs to lifecycle-owned asynchronous work. A queued metadata upload that must survive ordinary process loss may fit WorkManager. A user-visible ongoing activity may require a foreground service with the correct type and notification, while an exact alarm has a separate restricted contract. These mechanisms are not interchangeable ways to keep a process alive. Start with whether the work can be deferred, whether the user expects continuous visible progress, and which platform capability it needs. Choose the mechanism from those requirements rather than from a familiar class name.
Make a worker safe to rerun
A worker can stop and run again, so its business operation must tolerate repetition. Persist the operation identity and progress outside the worker object; constructor fields disappear with the process. Return retry for a genuinely temporary failure, failure for an unrecoverable input or policy condition, and success when the durable operation is complete or already complete. Do not swallow coroutine cancellation. Keep input data small and pass an id that resolves the real payload from storage. Never claim exactly-once remote execution merely because one local work request was enqueued.
interface PendingSync { suspend fun drain() }
class SyncWorker(
context: Context,
params: WorkerParameters,
private val sync: PendingSync
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
sync.drain()
Result.success()
} catch (e: CancellationException) {
throw e
} catch (e: IOException) {
Result.retry()
}
}
Use a compatible work-runtime-ktx dependency and AndroidX Work imports. Because the constructor has a third dependency, register a custom WorkerFactory or supported DI worker integration; the default factory cannot instantiate it. PendingSync must implement idempotency and classify permanent failures rather than labeling all errors as I/O retries.
Express constraints and duplicate policy
Constraints describe conditions such as network availability, not an execution deadline. A request becomes eligible when its conditions are satisfied, but Android still schedules actual execution. Unique work can prevent accidental parallel chains under a stable name. KEEP retains existing unfinished work, which suits some queue-draining designs, but a new request arriving while a drain finishes requires careful handoff so pending records are not stranded. The durable queue remains the authority; scheduling is a signal to make progress, not the only copy of an operation.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"reading-sync", ExistingWorkPolicy.KEEP, request
)
Run this after persisting pending operations and configuring the worker factory above. CONNECTED does not guarantee backend reachability, so the worker still handles I/O failure. Use application infrastructure to reconcile pending rows after interruptions and choose the unique-work policy to fit that design.
Respect scheduling and foreground restrictions
Periodic WorkManager work has a minimum repeat interval of fifteen minutes and is inexact; it cannot promise a notification at an exact wall-clock minute. Expedited work is quota-limited and needs an explicit fallback policy. Apps targeting API 31 or higher face foreground-service background-start restrictions with specific exceptions, and while-in-use permissions add further limits. Foreground service types, time limits, and quotas vary across later platform and target combinations. Consult the current official matrix before release instead of treating a service or expedited request as a universal bypass.
Observe durable outcomes and test interruption
WorkInfo reports scheduler state, while domain storage should explain whether each user operation is queued, synchronized, or requires intervention. These are related but not identical: one worker may process several records. Test loss of connectivity, cancellation, retry after partial progress, process recreation, and duplicate scheduling. A force-stopped app is subject to user-imposed restrictions and should not be promised immediate execution. Keep retries bounded by business policy, expose actionable permanent errors, and measure queue age so repeated failure does not remain invisible behind a reassuring spinning icon.
Common errors and fixes
A worker fails to instantiate
- Why it happens
- An injected constructor lacks a registered worker factory.
- How to fix it
- Configure WorkerFactory or the supported DI integration.
- Prevent it next time
- Test worker creation through WorkManager, not only direct construction.
A periodic reminder fires late
- Why it happens
- Inexact scheduling was mistaken for an exact alarm.
- How to fix it
- Use the platform mechanism appropriate to the timing requirement.
- Prevent it next time
- Document timing tolerance and verify eligibility rules.
Retries upload a record twice
- Why it happens
- The remote operation lacks durable deduplication.
- How to fix it
- Retain stable operation ids and enforce server idempotency.
- Prevent it next time
- Test interruption after server acceptance but before local acknowledgment.
Check your understanding
1. A team wants WorkManager to execute every thirty seconds. Explain the mismatch and ask which requirement should drive the replacement.
Show worked solution
Periodic work has a fifteen-minute minimum and remains inexact. Determine whether this is foreground UI polling, genuinely user-visible ongoing work, or a deferrable synchronization requirement. Choose an appropriate mechanism and battery policy from that classification rather than forcing a persistent scheduler to behave like a timer.
2. The backend accepts an upload, then the worker stops before local acknowledgment. Describe the next run.
Show worked solution
It finds the same pending operation id and retries using that identity. The backend returns the already-established result instead of creating a duplicate. The client then records completion atomically. This is why scheduler uniqueness alone is insufficient: the ambiguous outcome crosses the network boundary.
Key takeaways
- Durability belongs in stored operation state.
- Scheduling constraints are not deadlines.
- Platform limits and retry semantics must be reviewed together.