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
}
}
}
- 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);
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);
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().