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

Explanation: Because the UI never touches the network response directly, the exact same RecipeListActivity code path works whether that data arrived five seconds ago from the server or five days ago and has sat in Room ever since with the device fully offline.

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.
            }
        });
    }
}
Explanation:
  • 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:

ApproachHow it works
Timestamp fieldStore a lastFetchedAt value; refresh if it's older than some threshold (e.g., 15 minutes)
Pull-to-refresh + auto-refreshAlways 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.

Whichever policy is chosen, pick it deliberately and document it — silently overwriting a user's local edit with no explanation is a common source of "my changes disappeared" bug reports.

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.

WorkManager (periodic, network-constrained) └─ RecipeSyncWorker.doWork() └─ RecipeRepository.refreshFromNetwork() └─ writes into Room └─ UI (already observing Room) updates itself — no direct link needed

Hands-On Practice

  1. Confirm RecipeBox's recipe list screen reads only from Room's LiveData, never directly from a Retrofit callback.
  2. Add a lastFetchedAt field and skip refreshing if it's less than 15 minutes old.
  3. Turn on airplane mode and confirm the recipe list still displays previously-synced data without errors.
Congratulations, you've completed the Android (Java) track! From your first Activity all the way through architecture, dependency injection, and offline-first design, RecipeBox has been the thread connecting every lesson. Whenever the Kotlin and Jetpack Compose tracks are ready, the Android Home page is the place to pick up the next one.