Android SharedPreferences Tutorial and Example

Using WorkManager for background tasks in Android apps

Modern Android applications often need to perform operations when the user is not actively interacting with the screen. Whether syncing records with a remote server, uploading sensor readings, or processing queued images, reliable background execution is essential. WorkManager is the Jetpack library designed to handle exactly these scenarios, abstracting the complexities of JobScheduler, AlarmManager, and foreground services behind a clean, coroutine-friendly API.

Developers building apps for Australian users face particular considerations around connectivity and data costs. With the National Broadband Network reaching most urban centres and the rollout of 5G across Sydney, Melbourne, and Brisbane, expectations for always-on functionality are high. At the same time, regional users in places like Perth or Hobart often rely on patchier mobile coverage, which makes resilient offline-first design a practical necessity. WorkManager handles these conditions gracefully through its built-in constraint system.

This guide walks through the core concepts of the library, from setting up dependencies and writing your first Worker class, to scheduling periodic tasks, chaining work, and observing status changes. Code samples target Kotlin, but the same patterns translate directly to Java projects.

Setting up WorkManager in your project

Adding WorkManager to a module requires only one Gradle line in the dependencies block of your app-level build.gradle file:

implementation("androidx.work:work-runtime-ktx:2.9.0")

The ktx variant provides Kotlin extensions and coroutine support. For projects that rely on Java, the regular work-runtime artefact is sufficient. WorkManager registers itself automatically through a content provider merged from the manifest, so no further plumbing is required in most cases.

For projects that need custom configuration, such as a bespoke executor or a WorkerFactory, you can disable the default initializer in the manifest and supply configuration programmatically. This is useful in applications that already manage their own dependency injection graph or that want to control the threading model across debug and release builds. Useful boilerplate templates for this pattern can be found through curated myeasyprojects.net repositories, which provide ready-to-copy Android snippets.

Creating your first Worker class

A Worker is where the actual business logic lives. The simplest form extends CoroutineWorker and overrides doWork():

class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
    override suspend fun doWork(): Result {
        return try {
            // perform the task
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

Returning Result.success() signals a successful outcome, while Result.failure() tells WorkManager to drop the task permanently. Result.retry() requests another run, subject to the backoff policy configured on the work request. Choosing the right return value matters because it affects how aggressively the system reschedules failed tasks, which directly influences battery consumption on devices used throughout a long Australian commute.

For one-off tasks that cannot be expressed as suspending functions, Worker (the non-coroutine variant) is still available. It executes on a background thread pool by default and uses Worker.doWork() returning a ListenableWorker.Result. The newer RxWorker integrates with RxJava for projects that prefer reactive streams.

Scheduling one-time and periodic work

To enqueue a task, build a OneTimeWorkRequest and pass it to the WorkManager singleton:

val request = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build())
    .build()

WorkManager.getInstance(context).enqueue(request)

For recurring jobs, swap to PeriodicWorkRequestBuilder, which takes an interval in minutes. The minimum interval is 15 minutes, matching Android's scheduling granularity. Background periodic work is well suited to syncing calendar entries, refreshing cached news feeds, or polling the Australian Bureau of Meteorology API for updated weather alerts.

A comparison helps clarify when to reach for which Android scheduling mechanism:

Mechanism Best suited to Survives reboot Minimum interval API level
WorkManager Deferrable, guaranteed work Yes 15 min 14+
JobScheduler Low-level scheduling control Yes None 21+
AlarmManager Exact-time wake-ups No (unless using setExactAndAllowWhileIdle) None 1+
Foreground Service Long-running user-visible tasks Yes None 5+
Handler/ThreadPool In-process short work No N/A 1+

WorkManager is the recommended choice for anything that does not need to run at a precise wall-clock moment and that should persist across device restarts.

Applying constraints

Constraints let WorkManager wait for the right conditions before executing a task. Common options include network type, charging state, battery level, and storage availability. Stacking these is straightforward through the Constraints.Builder fluent API.

A practical example is an upload worker that should only run when the device is on unmetered Wi-Fi, which is a frequent concern for users on limited mobile plans in regional Western Australia or the Northern Territory. Adding setRequiredNetworkType(NetworkType.UNMETERED) to the constraints guarantees the worker waits until the device joins an appropriate network.

Other useful constraints include setRequiresBatteryNotLow(true) for heavy operations and setRequiresCharging(true) for tasks such as database migrations or large file uploads. Backoff policies can be configured with setBackoffCriteria(), supporting both linear and exponential strategies with a minimum delay of ten seconds.

Chaining work into workflows

Complex background pipelines often require multiple steps executed in a specific order. WorkManager supports this through WorkContinuation:

WorkManager.getInstance(context)
    .beginUniqueWork("upload_chain", ExistingWorkPolicy.KEEP,
        OneTimeWorkRequestBuilder<CompressWorker>().build())
    .then(OneTimeWorkRequestBuilder<UploadWorker>().build())
    .then(OneTimeWorkRequestBuilder<NotifyWorker>().build())
    .enqueue()

This pattern is handy for processing photos before upload, then sending a notification when finished. The ExistingWorkPolicy parameter controls what happens when the same chain is enqueued while still running: KEEP ignores the new request, REPLACE cancels the old chain, and APPEND combines them.

For parallel branches, pass a listOf to then rather than a single request. WorkManager coordinates the parallel work and continues to the next step once every branch reports success.

Observing work status

Live updates on running tasks are surfaced through the WorkInfo API. Each enqueued request produces one or more WorkInfo records whose state transitions from ENQUEUED through RUNNING to a terminal SUCCEEDED, FAILED, or CANCELLED state.

In a Compose or View-based UI, observing is usually done through a Flow<WorkInfo> returned by WorkManager.getWorkInfoByIdFlow(). The flow emits every time the status changes, making it trivial to render progress indicators or surface error messages. This is useful for downloads, where Australian users expect to see the percentage remaining on their screens.

For batch operations, getWorkInfosByTagFlow() returns a combined flow of all work sharing a particular tag. Tags are arbitrary strings attached at build time and act as a lightweight grouping mechanism.

Handling retries and cancellations

By default, Result.retry() triggers WorkManager's exponential backoff. The initial delay and policy can be customised through setBackoffCriteria. Maximum backoff time is capped at five hours, which prevents runaway scheduling on devices left idle for days.

Cancelling work is done by ID or by tag using cancelWorkById or cancelAllWorkByTag. Once cancelled, the worker receives a call to onStopped() if it is currently running, giving it a chance to clean up resources before exiting. Long-running operations should periodically check isStopped and break out of loops cleanly to avoid leaking memory.

For apps that must comply with the Australian Privacy Principles when processing user data in the background, it is wise to log cancellations and final states. This provides an auditable trail of what ran, when, and with what outcome, which can be valuable when responding to enquiries from the Office of the Australian Information Commissioner.

WorkManager brings order to the messy reality of background execution on Android. With its constraint system, retry logic, and chaining primitives, even complex workflows can be expressed in a handful of lines that survive reboots, respect battery limits, and behave consistently across the wide range of devices used from Darwin to Hobart.