WorkManager (Java)

WorkManager solves a very specific problem: how do you schedule background work that is guaranteed to run – even if the app is closed, the process is killed, or the device restarts – while still respecting the OS's battery-saving rules automatically? That's exactly what RecipeBox needs for "eventually sync any recipes the user added while offline", without babysitting API-level differences yourself.

Why WorkManager Instead of Rolling Your Own

As covered in AlarmManager & JobScheduler, WorkManager doesn't reinvent background scheduling from scratch – internally it picks JobScheduler, AlarmManager, or a custom fallback depending on the device's API level, and exposes one consistent API on top. You describe the work and its constraints; WorkManager figures out the best way to actually run it.

Defining a Worker

In Java, background work is defined by extending Worker and overriding doWork(). Unlike Kotlin's coroutine-based CoroutineWorker, Java's Worker runs synchronously on a background thread that WorkManager provides for you – there's no need to manage your own thread inside it.

// RecipeSyncWorker.java
public class RecipeSyncWorker extends Worker {

    public RecipeSyncWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Result doWork() {
        try {
            List<Recipe> pending = RecipeRepository.getInstance(getApplicationContext())
                    .getUnsyncedRecipes();
            for (Recipe recipe : pending) {
                RecipeApiClient.upload(recipe);
            }
            return Result.success();
        } catch (Exception e) {
            return Result.retry(); // WorkManager will retry with backoff
        }
    }
}
Explanation:
  • doWork() runs entirely on a background thread – blocking calls (like a network upload) are safe here.
  • Returning Result.success(), Result.failure(), or Result.retry() tells WorkManager how to treat the outcome.
  • Unlike a plain Thread, this work survives process death and is rescheduled by the OS as needed.

One-Time vs. Periodic Work

WorkManager supports both a single run and a recurring schedule:

// One-time: sync right now, in the background
OneTimeWorkRequest syncNow = new OneTimeWorkRequest.Builder(RecipeSyncWorker.class)
        .build();
WorkManager.getInstance(context).enqueue(syncNow);
// Periodic: sync roughly once a day
PeriodicWorkRequest dailySync = new PeriodicWorkRequest.Builder(
        RecipeSyncWorker.class, 1, TimeUnit.DAYS)
        .build();

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "recipe_daily_sync",
        ExistingPeriodicWorkPolicy.KEEP,
        dailySync);
Explanation: enqueueUniquePeriodicWork() with a unique name and ExistingPeriodicWorkPolicy.KEEP prevents scheduling a duplicate daily sync every time this code happens to run again (e.g. on every app launch).

Adding Constraints

Real sync work usually shouldn't run under just any condition. WorkManager lets you attach Constraints so the OS only runs the work when they're satisfied:

// Only sync when on WiFi and charging
Constraints constraints = new Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)
        .setRequiresCharging(true)
        .build();

PeriodicWorkRequest dailySync = new PeriodicWorkRequest.Builder(
        RecipeSyncWorker.class, 1, TimeUnit.DAYS)
        .setConstraints(constraints)
        .build();

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "recipe_daily_sync",
        ExistingPeriodicWorkPolicy.KEEP,
        dailySync);
Why this matters: RecipeBox users don't want a large photo sync eating their mobile data or draining the battery mid-day – WorkManager simply waits until the constraints are met, however long that takes.

Enqueueing Work

All work – one-time or periodic, with or without constraints – goes through the same call:

WorkManager.getInstance(context).enqueue(workRequest);

WorkManager persists this request to its own internal database, so it survives app restarts and even device reboots.

Observing Work Status

To show the user feedback like "Syncing recipes..." while a Worker runs, observe its status as LiveData<WorkInfo> (see ViewModel & LiveData for a refresher on observing LiveData):

WorkManager.getInstance(context)
        .getWorkInfoByIdLiveData(syncNow.getId())
        .observe(this, workInfo -> {
            if (workInfo == null) return;
            if (workInfo.getState() == WorkInfo.State.RUNNING) {
                syncStatusText.setText("Syncing recipes...");
            } else if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
                syncStatusText.setText("All recipes synced");
            }
        });

Troubleshooting

Periodic work runs more often than expected? → Check for duplicate enqueue() calls without a unique name; always prefer enqueueUniquePeriodicWork().

Work never seems to start? → Its constraints (e.g. charging + WiFi) may not be satisfied yet – relax them temporarily while testing.

Worker keeps retrying forever? → Make sure failures that shouldn't retry return Result.failure(), not Result.retry().

WorkManager is the right tool for deferrable background work. For work you need to do immediately but off the main thread, continue to Threads, Handlers, Executors.