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();
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
}
}
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));
| Concern | Loader / CursorLoader (deprecated) | Room + ViewModel + LiveData (recommended) |
|---|---|---|
| Survives rotation | Yes, but via 3 callback methods | Yes, via ViewModel automatically |
| Auto-refresh on data change | Yes, manually wired to a ContentObserver | Yes, built into the DAO's LiveData return type |
| API status | Officially deprecated | Actively maintained Jetpack components |
| Boilerplate | High — full LoaderCallbacks interface | Low — one DAO method, one observe() call |