Android SharedPreferences Tutorial and Example

Scheduling periodic work with WorkManager in Android

Modern Android applications rarely live in isolation. From ride-sharing platforms running through Brisbane streets to weather services feeding surf reports along the Gold Coast, users expect apps to keep working even when the screen is off, the device is in a pocket, or the user is catching a tram down Flinders Street. That expectation pushed Google to consolidate its background processing APIs around WorkManager, the recommended way to schedule guaranteed, deferrable tasks.

Periodic scheduling sits at the heart of that system. Whether you are syncing training data for runners preparing for the City2Surf, refreshing cricket scores on a summer afternoon at the SCG, or pushing promotions tied to a user's local AEST timezone, WorkManager gives you a single API that handles constraints, retries and persistence across reboots. The sections that follow cover the conceptual model, the actual code, the constraint layer and the testing workflow you can drop into a production project today.

Why periodic background work matters for modern apps

Battery-aware scheduling has become a competitive feature rather than a nice-to-have. Australian developers shipping apps to a market where tap-and-go payments dominate and users routinely toggle between mobile data at a servo and home Wi-Fi need a scheduling primitive that behaves predictably when the network flaps. WorkManager fills that role by piggybacking on the JobScheduler API on Android 6.0 and above while providing a compatibility shim for older releases.

Periodic work is also the natural answer to tasks that should run regardless of whether the user opens the app that day. A news app refreshing headlines overnight in Sydney, a transport app pulling timetable updates for Melbourne's tram network, or a regional mining logistics tool syncing shift rosters from Perth to the Pilbara all rely on the same pattern. None of these jobs need to fire at a millisecond-precise instant, which is exactly the use case WorkManager is tuned for.

Core concepts behind WorkManager scheduling

The library models every scheduled unit as a Worker or its coroutine-friendly sibling CoroutineWorker. A Worker describes a single unit of work, while a WorkRequest describes when and under what constraints that worker should run. The two flavours relevant for periodic tasks are PeriodicWorkRequest, which fires at a fixed interval, and OneTimeWorkRequest, useful for follow-up tasks triggered by a periodic worker.

Three properties define every periodic request: the interval, the flex window, and the initial delay. The interval is the minimum gap between executions, expressed as a Duration. The flex window lets the system batch executions inside a defined sub-period at the end of the interval, which is invaluable when chaining several periodic workers together. Constraints are evaluated independently and include network type, charging state, battery level and idle trigger requirements.

When you combine these pieces, you get a builder pattern that reads almost like prose. A common pattern in scheduling apps that surface calendar reminders is to align periodic refresh with the moment a user finishes interacting with a datepickerdialog inside the app, which is why many tutorial collections cluster the two topics together. Once enqueued, WorkManager persists the request in an internal database, which means the work survives reboots, force-stops and app updates.

Building a periodic worker in Kotlin and Java

Writing the worker itself is the simplest part of the process. In Kotlin you extend CoroutineWorker and override doWork, returning Result.success(), Result.retry() or Result.failure() depending on the outcome. In Java you extend Worker and override doWork, but you have to manage threading manually because the default implementation runs on a background executor.

The following table maps the equivalent calls between the two languages for a fifteen-minute sync job.

Step Kotlin with CoroutineWorker Java with Worker
Class declaration class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) public class SyncWorker extends Worker { ... }
Work logic override suspend fun doWork(): Result { ... } @NonNull @Override public Result doWork() { ... }
Enqueue request PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES) new PeriodicWorkRequest.Builder(SyncWorker.class, 15, TimeUnit.MINUTES)
Constraint set Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED) new Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED)
Submission WorkManager.getInstance(context).enqueueUniquePeriodicWork("sync", ExistingPeriodicWorkPolicy.KEEP, request) WorkManager.getInstance(context).enqueueUniquePeriodicWork("sync", ExistingPeriodicWorkPolicy.KEEP, request);

Once the worker is enqueued with a unique work name, the system guarantees a single chain of instances, which simplifies retry logic and prevents overlapping executions that could hammer your backend.

Constraining periodic jobs with network and battery rules

Raw periodic requests are rarely useful on their own. Almost every production worker should declare at least one constraint, otherwise the system may execute the job while the device is on a congested regional train corridor with patchy reception, wasting both battery and data. The most common constraint is setRequiredNetworkType, which can be set to CONNECTED for any available network or UNMETERED for Wi-Fi only.

Battery and charging constraints are equally important for jobs that pull or upload large files. setRequiresBatteryNotLow skips the job when the battery drops below a healthy threshold, which matters for tradies and field workers who rely on their phones to log shifts across regional Western Australia. setRequiresCharging is appropriate for heavy backups that you only want to run overnight, matching the typical Australian off-peak charging habit.

Finally, the backoff policy determines how a failed execution retries. Exponential backoff suits network-bound tasks because it gives the user time to move back into coverage, while linear backoff suits jobs that depend on an external service that might take a known amount of time to recover. Tuning these settings is what separates a polite background app from one that wakes the device every fifteen minutes for no good reason.

Testing, observing and debugging scheduled workers

WorkManager ships with an in-app debugging tool called WorkManager Inspector, accessible through Android Studio's App Inspection tab. It lists enqueued work, lets you cancel individual requests and surfaces logs from individual executions, all without restarting the device. For automated testing, TestDriver and WorkManagerTestInitHelper let you advance a virtual clock and force workers to run synchronously, which is essential for CI pipelines hosted on the east coast where build agents already operate on AEDT.

When something goes wrong in production, the first stop should always be the worker's logs. Each WorkerResult is recorded with a timestamp, the constraint state at execution time and the failure reason. Pairing those logs with crash reporting gives you a complete picture of how a job performed across the diverse device fleet sold in Australia, from flagship Samsung and Pixel units in Sydney showrooms to mid-range Motorola handsets common in regional Queensland.

Persisting the work through reboots and config changes is what makes WorkManager worth the extra dependency. Once the periodic chain is in place, your code becomes resilient to almost every scenario users will throw at it, and you can focus on the actual business logic instead of fighting the platform.