Cloud Firestore (Java)

Cloud Firestore is Firebase's flexible, cloud-hosted NoSQL document database. Where Room (covered earlier in Data & Storage) stores recipes only on the device they were entered on, Firestore stores the same data on Google's servers and keeps every signed-in device in sync automatically. If a RecipeBox user signs in on a new phone, their saved recipes simply appear — nothing to export, back up, or transfer by hand.

Room (local)

SQLite on one device only. Gone if the device is lost.

vs

Firestore (cloud)

Synced across every signed-in device, backed up automatically.

The Collection / Document Model

Firestore organizes data into collections of documents. A collection is a named bucket (like "recipes"); a document is one JSON-like record inside it, identified by its own ID. For RecipeBox, every user's saved recipe becomes one document in a single recipes collection, tagged with the owner's UID from Firebase Auth so it can later be filtered per user.

// Recipe.java — plain fields Firestore can map straight to/from a document
public class Recipe {
    public String id;
    public String title;
    public String ingredients;
    public String steps;
    public String photoUri;
    public String category;
    public int prepMinutes;
    public String ownerUid;   // the Firebase Auth UID that owns this recipe

    public Recipe() { }  // required empty constructor for Firestore's automatic mapping
}
Explanation:
  • A collection named recipes holds one document per saved recipe, from every user of the app.
  • ownerUid stores which signed-in user created the recipe, so later queries can filter to "recipes belonging to me".
  • Firestore needs a no-argument constructor to automatically convert a document back into a Recipe object.

Writing Data

Adding a new recipe is a single call to .add() on a collection reference. It returns a Task<DocumentReference> — Firestore generates a unique document ID for you automatically.

FirebaseFirestore firestoreDb = FirebaseFirestore.getInstance();

Recipe recipe = new Recipe();
recipe.title = "Grandma's Chicken Curry";
recipe.ingredients = "chicken, onion, tomato, spices";
recipe.steps = "1. Saute onions... 2. Add chicken...";
recipe.category = "Dinner";
recipe.prepMinutes = 40;
recipe.ownerUid = FirebaseAuth.getInstance().getCurrentUser().getUid();

firestoreDb.collection("recipes")
    .add(recipe)
    .addOnSuccessListener(documentReference -> {
        Log.d("RecipeBox", "Saved with id: " + documentReference.getId());
    })
    .addOnFailureListener(e -> {
        Log.e("RecipeBox", "Save failed", e);
    });
Explanation:
  • .collection("recipes") points at the collection; .add(recipe) creates a brand-new document with an auto-generated ID.
  • The write happens asynchronously — addOnSuccessListener and addOnFailureListener run once Firestore's servers confirm the result, never blocking the calling thread.

Real-Time Listening with addSnapshotListener()

Firestore's signature feature is that a query can stay "alive". Instead of fetching data once, addSnapshotListener() registers a callback that fires immediately with the current data, and then fires again automatically every time matching data changes on the server — even if the change came from a different device.

firestoreDb.collection("recipes")
    .addSnapshotListener((querySnapshot, error) -> {
        if (error != null) {
            Log.e("RecipeBox", "Listen failed", error);
            return;
        }
        List<Recipe> recipes = new ArrayList<>();
        for (DocumentSnapshot doc : querySnapshot.getDocuments()) {
            Recipe recipe = doc.toObject(Recipe.class);
            recipe.id = doc.getId();
            recipes.add(recipe);
        }
        adapter.submitList(recipes);  // RecyclerView updates on its own
    });
Explanation:
  • This one listener replaces both the initial load and every future refresh — no manual "pull to refresh" logic needed.
  • If a recipe is edited on the user's tablet, this exact callback fires on their phone moments later with the updated list.
  • doc.toObject(Recipe.class) maps a document's fields back onto the Recipe class using its public fields.

Querying: whereEqualTo() and orderBy()

Rather than downloading every user's recipes and filtering in Java, Firestore lets the query itself ask only for what's needed — cheaper, faster, and it works together nicely with addSnapshotListener() above.

// Only the current user's recipes, alphabetically by title
String myUid = FirebaseAuth.getInstance().getCurrentUser().getUid();

firestoreDb.collection("recipes")
    .whereEqualTo("ownerUid", myUid)
    .orderBy("title")
    .addSnapshotListener((querySnapshot, error) -> {
        if (error != null) return;
        List<Recipe> myRecipes = new ArrayList<>();
        for (DocumentSnapshot doc : querySnapshot.getDocuments()) {
            myRecipes.add(doc.toObject(Recipe.class));
        }
        adapter.submitList(myRecipes);
    });
Explanation:
  • .whereEqualTo("ownerUid", myUid) restricts the query to documents belonging to the signed-in user.
  • .orderBy("title") asks Firestore's server to return results already sorted — no in-memory Collections.sort() required.

Security Rules: The Real Enforcement

The whereEqualTo("ownerUid", myUid) filter above is a convenience for the app, not a security boundary — a modified client could simply omit it and request every user's recipes. The actual protection lives in Firestore Security Rules, which run on Google's servers and are evaluated on every single read and write, regardless of what the client asks for.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /recipes/{recipeId} {
      allow read, write: if request.auth != null
                       && request.auth.uid == resource.data.ownerUid;
    }
  }
}
Explanation: This rule rejects any request unless the caller is signed in (request.auth != null) and the document's ownerUid matches their own UID. Even if a client tried to bypass the app's query filters entirely, Google's servers would still refuse the request — this is what actually keeps one user's recipes private from another's.
Firestore's collections and documents model works beautifully for RecipeBox, but it is not Firebase's only database option. The next lesson, Realtime Database, looks at Firebase's older JSON-tree database and when it might still make sense to use it instead.