Background Work with WorkManager: learning goals
Schedule a refresh that should remain eligible after the user leaves the screen. Choose work based on its required lifetime, make it safe to repeat, and give failure and cancellation different meanings.
Project setup and scope
Build the mental model
A screen coroutine is appropriate for work whose lifetime belongs to that screen. Persistent deferrable work, such as refreshing a cache under a network constraint, belongs to WorkManager. Exact alarms, a live countdown and user-visible long-running work have different requirements; a periodic Worker is not a universal timer.
A WorkRequest describes the operation and its constraints. Enqueuing a unique name controls duplicate pending work; it does not make a remote side effect exactly once. The worker can run again after interruption or retry, so refresh/upsert operations need stable IDs and repeat-safe behavior.
Return success when the intended operation is complete, retry for a recoverable failure, and failure for a condition that cannot be repaired by repeating unchanged input. Use backoff and an explicit attempt policy rather than retrying every invalid request indefinitely. Constraints describe eligibility, not a promised wall-clock start time.
The worker should call the same repository used elsewhere. UI observes local committed results and a deliberate refresh status; it should not depend on a screen callback that may no longer exist. Cancellation stops cooperative work; do not swallow CancellationException in a catch-all result wrapper.
Choose the right approach
| Need | Approach |
|---|---|
| Fetch while a detail destination is active | ViewModel-owned coroutine with cancellation and screen state. |
| Persistent cache refresh with network eligibility | Unique WorkManager request with a network constraint. |
| Update a visible countdown | A lifecycle-aware UI clock/state strategy; WorkManager is not precise enough. |
| Retry a write with a remote side effect | Design a stable operation ID/idempotency contract before retrying. |
| Remote reports invalid request | Surface/correct the input; repeating forever is not recovery. |
Predict and trace the behavior
Background Work with WorkManager
Predict what happens next. Follow the diagram, then explain the change in your own words.
Suspension leaves room for other work
Waiting for resultResume
These are logical lanes. A coroutine is not a dedicated thread; suspension does not automatically move work off Main.
A suspending operation can wait without occupying the calling thread. Coroutine lifetime belongs to its scope.
Explore this general model independently. The lesson sequence below follows its own example.
Launch: A coroutine begins in an appropriate lifecycle-owned scope.
Enqueue
The app submits a unique request named refresh-topics and a CONNECTED network constraint.
Worked Kotlin example
import android.content.Context
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import java.io.IOException
import kotlinx.coroutines.delay
class RefreshWorker(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
// Deterministic fake; replace this block with repository.refresh().
delay(300) // Cancellable suspension, not Thread.sleep.
if (inputData.getBoolean("simulateFailure", false)) {
throw IOException("Simulated temporary network failure")
}
Result.success()
} catch (_: IOException) {
// Three total attempts in this classroom policy.
if (runAttemptCount < 2) Result.retry() else Result.failure()
}
// CancellationException is not caught and propagates correctly.
}
fun scheduleTopicRefresh(context: Context, simulateFailure: Boolean = false) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<RefreshWorker>()
.setConstraints(constraints)
.setInputData(workDataOf("simulateFailure" to simulateFailure))
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"refresh-topics",
ExistingWorkPolicy.KEEP,
request,
)
}
// Call scheduleTopicRefresh(context) for success, or pass true for retry/failure.
// In the capstone, repository.refresh() should upsert stable IDs into Room.
// Supply that repository through an explicitly configured worker factory.- KEEP retains an existing unfinished request under the same unique name. Choose a different policy only when its cancellation/replacement behavior fits the use case.
- A satisfied network constraint is not proof that the API is reachable. Even then, timeouts and server failures still need handling.
- The fake worker suspends, then succeeds or simulates an IOException. Its bounded attempt policy uses runAttemptCount; default WorkManager backoff applies between attempts. Only IOException is caught, so cancellation propagates.
- Run the fake scheduling flow first. Then replace its fake block with the production repository using an explicit worker factory or the later Hilt integration. The fake demonstrates execution outcomes; it intentionally does not write topic data.
Student practice and expected behavior
Add a manual 'schedule refresh' action to the offline tracker. Implement a worker using a deterministic fake source, a recoverable failure mode, and Room upserts. Observe the same data flow used by a foreground refresh.
- Enqueue while offline: work remains pending while its network constraint is unmet.
- Restore connectivity: the eligible request eventually executes; do not measure success by an exact second.
- Submit the same unique request repeatedly: pending work follows KEEP semantics.
- Repeat a successful refresh: topic identities remain stable and duplicate records do not appear.
- Cancel a running request and simulate a timeout separately: cancellation propagates, while a recoverable timeout follows the stated retry policy.
Write down the input, expected outcome and observed outcome for each check. Include a screenshot, a short recording or a nonpersonal diagnostic trace appropriate to the behavior you are proving.
Debugging clinic
- Unique work prevents certain scheduling duplicates, not duplicate side effects at an external server.
- A worker's input/output Data is for small supported values; large payloads belong in storage and can be referenced by ID.
- Do not create a ViewModel or call UI callbacks inside a Worker. Its lifetime is independent of the visible destination.
- A network constraint can change while work runs; design interruption and recovery rather than assuming permanent connectivity.
Instructor prompts
- Compare refresh-on-open with refresh-after-leaving. Ask which lifetime each operation needs.
- Have one team repeat the same operation twice and another interrupt it; inspect whether IDs and writes remain correct.
- Draw success, retry, failure and cancellation as distinct outcomes; ask which are terminal for this request.
Teach-back checkpoint
Quick check and references
Quick check
1. What does a network constraint promise?
2. Why make refresh idempotent?
Continue with the phase capstone after demonstrating the practice checks. Use the official documentation to verify APIs against your project's selected dependency versions.