Phase 4 · Data & PersistenceModule 30~40 min read

Background Work with WorkManager

Schedule repeat-safe persistent refresh work with unique requests, network constraints, cancellation and an explicit retry policy.

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

Add the compatible androidx.work:work-runtime-ktx dependency from the current official setup guide. This example includes a standard CoroutineWorker using a fake refresh operation so scheduling and retry can be tried before repository integration. Constructor injection requires an explicitly configured WorkerFactory; Hilt integration is introduced in Phase 6.

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

NeedApproach
Fetch while a detail destination is activeViewModel-owned coroutine with cancellation and screen state.
Persistent cache refresh with network eligibilityUnique WorkManager request with a network constraint.
Update a visible countdownA lifecycle-aware UI clock/state strategy; WorkManager is not precise enough.
Retry a write with a remote side effectDesign a stable operation ID/idempotency contract before retrying.
Remote reports invalid requestSurface/correct the input; repeating forever is not recovery.

Predict and trace the behavior

Android concept labInteractive walkthrough

Background Work with WorkManager

Predict what happens next. Follow the diagram, then explain the change in your own words.

Representative concept map

Suspension leaves room for other work

async
Logical time
UI thread
InputLayoutInputDraw
Coroutine
RunSuspended
Waiting for result
Resume
Owned scopeResult → stateCancelled scope → cancel work

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.

Lesson sequence1 / 4
Step 1 of 4

Enqueue

The app submits a unique request named refresh-topics and a CONNECTED network constraint.

Up nextWait
A teaching simulation; diagrams illustrate the concept. Run the Kotlin examples in Android Studio.

Worked Kotlin example

RefreshScheduling.kt
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.
Focused teaching example. Keep your generated package/theme and complete the explicitly labeled integration points.
  1. 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.
  2. A satisfied network constraint is not proof that the API is reachable. Even then, timeouts and server failures still need handling.
  3. 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.
  4. 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

  1. Compare refresh-on-open with refresh-after-leaving. Ask which lifetime each operation needs.
  2. Have one team repeat the same operation twice and another interrupt it; inspect whether IDs and writes remain correct.
  3. Draw success, retry, failure and cancellation as distinct outcomes; ask which are terminal for this request.

Teach-back checkpoint

Ask a student to describe one incorrect approach, reproduce the resulting behavior, and explain the repair using ownership, lifetime, constraints or trust boundaries from this lesson.

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.