Cursor & Loaders (Legacy) (Java)

This lesson is different from most of this track: it covers a pattern you should recognize when reading older code, not one you should reach for in a new RecipeBox feature. Loader and CursorLoader are officially deprecated APIs from before ViewModel and LiveData existed. We cover them briefly so the terminology is familiar — the actual recommendation, given up front, is: use Room + ViewModel + LiveData for anything this lesson describes.

Recap: What a Cursor Is

As covered in SQLiteOpenHelper (Java), a Cursor is the result-set object a database or ContentProvider query returns. It doesn't hold all the rows in memory at once — it's a movable pointer into the result set. You step through it with moveToFirst() / moveToNext() and read each row's columns by index or name, as shown when RecipeBox fetched all recipes:

Cursor cursor = db.query("recipes", null, null, null, null, null, "title ASC");
while (cursor.moveToNext()) {
    String title = cursor.getString(cursor.getColumnIndexOrThrow("title"));
}
cursor.close();
A Cursor by itself knows nothing about Activity/Fragment lifecycles — if you query it directly in onCreate() and the screen rotates, you'd naively re-query from scratch, or worse, leak the old Cursor if you forget to close it.

The Problem Loaders Were Built to Solve

Before ViewModel existed, Android needed some official way to keep a Cursor's lifecycle in sync with an Activity or Fragment — reloading data automatically when it changed, and avoiding leaks or crashes across configuration changes like rotation. That was the job of the Loader API, and specifically CursorLoader for database/ContentProvider results.

What CursorLoader Code Used to Look Like

You will not be asked to write this in RecipeBox — this is here purely so you can recognize the pattern in legacy codebases you might encounter:

// Legacy pattern — shown for recognition only, not recommended
public class LegacyRecipeListActivity extends AppCompatActivity
        implements LoaderManager.LoaderCallbacks<Cursor> {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportLoaderManager().initLoader(0, null, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(this, recipesUri, null, null, null, "title ASC");
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        // adapter.swapCursor(cursor) — update the list UI here
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        // adapter.swapCursor(null) — release the Cursor reference
    }
}
Be honest about this API: LoaderManager and CursorLoader are officially deprecated. Three callback methods just to keep one Cursor in sync with the lifecycle is exactly the kind of ceremony that ViewModel + LiveData was introduced to remove.

Why Room + ViewModel + LiveData Replaced This

As covered in ViewModel & LiveData and Room Database (Java), a ViewModel already survives configuration changes, and a Room DAO method that returns LiveData<List<Recipe>> already re-emits automatically whenever the underlying table changes — which is precisely what CursorLoader was trying to achieve, just with far less boilerplate and no deprecated APIs:

// Modern equivalent — RecipeViewModel exposing Room's LiveData
public class RecipeViewModel extends AndroidViewModel {
    private final LiveData<List<Recipe>> allRecipes;

    public RecipeViewModel(Application app) {
        super(app);
        RecipeDao dao = AppDatabase.getInstance(app).recipeDao();
        allRecipes = dao.getAllRecipes();
    }

    public LiveData<List<Recipe>> getAllRecipes() { return allRecipes; }
}

// In the Activity — one observe() call replaces the entire LoaderCallbacks interface
viewModel.getAllRecipes().observe(this, recipes -> adapter.submitList(recipes));
ConcernLoader / CursorLoader (deprecated)Room + ViewModel + LiveData (recommended)
Survives rotationYes, but via 3 callback methodsYes, via ViewModel automatically
Auto-refresh on data changeYes, manually wired to a ContentObserverYes, built into the DAO's LiveData return type
API statusOfficially deprecatedActively maintained Jetpack components
BoilerplateHigh — full LoaderCallbacks interfaceLow — one DAO method, one observe() call
Direct recommendation: anything CursorLoader used to handle in RecipeBox, reach for Room + ViewModel + LiveData instead — this lesson exists so the terminology and legacy pattern are recognizable when you meet them in old code, not as a technique to build new features with.
Data storage covered so far handles data on the device. RecipeBox will eventually also need to talk to a server — for example, downloading a shared recipe. The next lesson, HTTP Basics & Threads, starts the Networking section by covering how to make network calls safely without freezing the UI.