Offline-First & Caching
Offline-first is a design principle, not a single API: the app should be fully usable — viewing, adding, editing recipes — even with no network connection at all, treating connectivity as an occasional bonus rather than a requirement. This final lesson ties together Room, Retrofit, and WorkManager, all covered earlier, into that single principle for RecipeBox.
Room as the Single Source of Truth
The core rule of offline-first: the UI only ever reads from Room. It never reads directly from a network response. The network's only job is to fetch fresh data and write it into Room — after that, Room is what the rest of the app sees.
Retrofit
Fetches from the network
Room
Single source of truth
UI
Observes Room only
The RecipeBox Sync Flow
// RecipeRepository.java
public class RecipeRepository {
private final RecipeDao dao;
private final RecipeApiService api;
// UI observes this directly — backed by Room, updates automatically on any insert
public LiveData<List<Recipe>> getRecipes() {
return dao.getAllRecipes();
}
// Called on app start / pull-to-refresh — writes into Room, never returned directly to the UI
public void refreshFromNetwork() {
api.getAllRecipes().enqueue(new Callback<List<Recipe>>() {
@Override
public void onResponse(Call<List<Recipe>> call, Response<List<Recipe>> response) {
if (response.isSuccessful() && response.body() != null) {
AppDatabase.databaseWriteExecutor.execute(() ->
dao.insertAll(response.body())); // Room emits the update; UI reacts automatically
}
}
@Override
public void onFailure(Call<List<Recipe>> call, Throwable t) {
// No network? The UI keeps showing whatever Room already has. Nothing breaks.
}
});
}
}
- getRecipes() never calls the network — it only reads Room, exactly as covered in Room Database (Java) and ViewModel & LiveData.
- A failed refreshFromNetwork() call is silently tolerated — the offline-first app degrades to "shows slightly older data," never to "shows nothing" or "crashes."
Handling Stale Data
Once Room is the source of truth, "how do I know this data is current?" becomes an explicit design question instead of an assumption. Two common, simple approaches:
| Approach | How it works |
|---|---|
| Timestamp field | Store a lastFetchedAt value; refresh if it's older than some threshold (e.g., 15 minutes) |
| Pull-to-refresh + auto-refresh | Always show cached Room data instantly, kick off refreshFromNetwork() in the background on every screen open |
A Brief Note on Conflict Resolution
When a recipe can be edited both offline (on-device, unsynced) and on the server, a genuine conflict becomes possible. RecipeBox doesn't need a sophisticated merge algorithm to start — a simple, explicit policy is enough:
Last-write-wins: whichever edit has the newer timestamp overwrites the other. Simple, and good enough for a single-user recipe app.
Server-wins: the server's copy is always treated as authoritative; local edits are re-applied on top after a sync.
WorkManager for Background Sync
The sync flow above still needs to run on some schedule, even when RecipeBox isn't open — exactly the job covered in WorkManager (Java). A periodic RecipeSyncWorker can call the same refreshFromNetwork() logic, guaranteed to run even across app restarts, respecting battery and network constraints automatically.
Hands-On Practice
- Confirm RecipeBox's recipe list screen reads only from Room's LiveData, never directly from a Retrofit callback.
- Add a lastFetchedAt field and skip refreshing if it's less than 15 minutes old.
- Turn on airplane mode and confirm the recipe list still displays previously-synced data without errors.