Cloud Firestore - Reading, Updating, and Deleting Data

  1. Objectives
  2. Prerequisites
  3. Codealong
  4. Reading and Displaying Data from the Cloud Firestore
  5. Using a StreamBuilder to create a List that updates in realtime
  6. Deleting documents
  7. Updating Firestore data
  8. Completed Code
  9. Conclusion

Objectives

Prerequisites

You must complete Cloud Firestore: Introduction and Adding Data prior to this lab.

Codealong

https://youtu.be/dm3XM46jAPM

Reading and Displaying Data from the Cloud Firestore

As we say in the previous lab, the Cloud Firestore organizes it’s data in collections of documents, and each document consists of key-value pairs called fields and values.

Many apps display lists of data: teams in a sports league, sites in a city, matches for your profile. We covered how to display lists in the Lists and Grids lab. But, what if your list of data is coming from the Cloud Firestore? Turns out, it is relatively easy to display Collections from the Firestore as a List or Grid and update the list in realtime when the Firestore data changes.

We are going to create a List that displays all the documents in in our ‘People’ collection from the previous lab. We are going to create a custom StatelessWidget for this and include it in our FirestoreAddScreen. We could do it all in FirestoreAddScreen, but a separate widget will help with code organization and will be reusable elsewhere in our app if needed.

Open firestore_add.dart. Create a blank Stateless widget called PeopleList using the VSCode blueprint:

class PeopleList extends StatelessWidget {
  PeopleList({super.key});

  @override
  Widget build(BuildContext context) {
    return Placeholder();
  }
}

Then, update _FirestoreAddScreenState’s build method to display the old ElevatedButton and the new widget in a column:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Add data to Firestore")),
      body: Center(
        child: Column(children: [
          ElevatedButton(
            onPressed: () => generateAndAddPerson(),
            child: const Text("Generate and add a person"),
          ),
          Expanded(child: PeopleList())
        ]),
      ),
    );
  }

Your “Add data to Firestore” screen should now look like this: Firestore list display starting point with placeholder

Using a StreamBuilder to create a List that updates in realtime

Dart (and Flutter), Java, and other languages have the concept of a stream. Streams in Dart provide an asynchronous sequence of data.

A stream is a sequence of asynchronous events. It is like an asynchronous Iterable—where, instead of getting the next event when you ask for it, the stream tells you that there is an event when it is ready.

Think of a stream as a road and you are working the toll booth. Occasionally, traffic (data) moves steadily down the road and you interact with each car. Other times, there is no traffic and you’re waiting for a car to arrive.

In Dart programming, Streams your code must effectively listen for data to arrive before doing anything. This kind of asynchronous programming can be very difficult to do properly.

Fortunately, Flutter has a widget that can help called StreamBuilder:

StreamBuilder is a versatile widget. It basically listens to a stream (a road of data), and re-builds when the data changes. We can use StreamBuilder to create a ListView that will update whenever the Firestore data changes.

First, add a reference to our “People” collection as the PeopleList widget. Remember, this reference is basically a pointer that allows our code to interact with the People collection in the cloud.

  class PeopleList extends StatelessWidget {
    PeopleList({super.key});

    // Reference to the Firestore "People" collection
    final peopleRef = FirebaseFirestore.instance.collection('People');


    @override
    Widget build(BuildContext context) {
      return Placeholder();
    }
  }

Next, we add the StreamBuilder. It has two parameters we care about:

The peopleRef.snapshots() stream changes any time the “People” collection changes. So, we are going to get an updated List of Documents with each Snapshot. We can thus use a ListView builder to generate widgets for each Document in the list.

Change the PeopleList widget to the following:

class PeopleList extends StatelessWidget {
  PeopleList({super.key});

  // Reference to the Firestore "People" collection
  final peopleRef = FirebaseFirestore.instance.collection('People');

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: peopleRef.snapshots(), //.snapshots() gives us a Stream
      builder: (context, snapshot) {
        // Make sure that the snapshot has data with it.
        // There may be no data while the network connection is initializing.
        // And sometimes the data is empty, like and empty street.
        if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
          return const Text("No data to show!");
        }

        // Here is the list of Documents from the People collection.
        var personDocs = snapshot.data!.docs;
        // Use a ListView.builder to generate a ListView
        // to display the People collection
        return ListView.builder(
            itemCount: personDocs.length,
            itemBuilder: ((context, index) => Card(
                  child: ListTile(
                    title: Text(
                        "${personDocs[index].get('first')} ${personDocs[index].get('last')}"),
                  ),
                )));
      },
    );
  }
}

Note how the builder property of the StreamBuilder is a function that returns a widget of some sort. One of those widgets is a ListView.builder(), which we covered in a previous lab, and is useful for creating a ListView from a Dart List<> of data.

Observe in the ListView.builder() how we create the title of the ListTile (the text that is displayed) using the Document data. To get a field from a document, we treat it like a Dart map object:

Go to your Firebase console and delete the People collection – don’t worry, our app will recreate it. Then add some using the button in your app. The list will update in real time!

Pretty cool! Now, the benefit of the StreamBuilder is that it listens in realtime at the cost of more resources consumed because you are listening to changes over a network and doing something every time the data changes.

If you are not interested in realtime updates and just want a list of data to display, peopleRef.get() is your friend. The get() method will return all the Documents in the collection. You can then display them directly in the ListView.builder(). Your Google skills will yield examples of doing this in a StatefulWidget.

We will discuss how to query for a subset of documents, say, everyone whose first name is “Raphael”, in a subsequent lab.

Deleting documents

Firestore implements the CRUD operations (Create, Read, Update, and Delete) provided by every database. We’ve shown how to create and read data, and now it’s time to delete.

We will make it such that tapping on a List item deletes it. Because our List is listening to the Firestore in realtime, it will update for us automatically once the data is deleted from the Firestore. We don’t need to worry about updating the list widget manually. So the flow of events is:

  1. User taps a list item to indicate it is deleted
  2. Our app tells Firestore to delete the item
  3. Firestore sends updates to the peopleRef.snapshots() stream
  4. Our StreamBuilder automatically rebuilds the list.

We need a way to detect taps. Fortunately, the ListTile widget has an onTap property. Let’s add it.

Add the following onTap property to the ListTile inside the ListView.builder()

  onTap: () {
    // Get a reference to the Document you want to delete.
    QueryDocumentSnapshot person = personDocs[index];
    
    // delete() is asynchronous. The .then() method says what to do when it completes.
    peopleRef.doc(person.id).delete().then(
        (value) => print("Deleted ${person.get('first')} ${person.get('last')}"),
        onError: (error, stackTrace) => print("Could not delete: $error}"));
},

There are a few items of note in this code:

That’s it. Note how the list updates automatically in response to the Firestore data being changed.

The two keys to deletion are: 1) getting the DocumentReference using the document’s unique ID, and 2) calling the .delete() method.

Updating Firestore data

Sometimes you will have an existing Firestore Document whose data you want to change. Perhaps one of our People changed their name, or had a birthday so their age is out of date. In this case, we want to Update their data.

In our example app, we’ll add a new field to indicate if we have visited a person’s work of art. This will be a boolean field that is initially false, and that we will allow the user to toggle between true or false through the UI.

First, let’s update our Person class in person.dart to add the new field:

import "dart:math";

import 'package:cloud_firestore/cloud_firestore.dart';

class Person {
  Person(
      {this.id,
      required this.first,
      required this.last,
      required this.age,
      required this.visited});

  String? id;
  final String first;
  final String last;
  int age;
  bool visited;

  /// Static generate a random person object from a preset list of names.
  static Person getRandomPerson() {
    final List<String> firstNames = [
      'Leonardo',
      'Bob',
      'Raphael',
      'Filippo',
      'Pablo',
      'Rembrandt'
    ];

    final List<String> lastNames = [
      'da Vinci',
      'Ross',
      'Santi',
      'Brunelleschi',
      'Picasso',
      'van Rijn'
    ];

    final random = Random();
    var first = firstNames[random.nextInt(firstNames.length)];
    var last = lastNames[random.nextInt(lastNames.length)];
    var age = random.nextInt(100);

    return Person(first: first, last: last, age: age, visited: false);
  }

  /// A utility method to convert our Person class into a Map
  /// with the same values. The Firestore library expects a Map.
  Map<String, Object?> toMap() {
    // The { } create a Map object, much like { } in Python creates a dictionary
    // This method creates and immediately returns the map.
    return {'first': first, 'last': last, 'age': age, 'visited': visited};
  }
}

Note the addition of the visited boolean class variable. We also tweaked the getRandomPerson() method to initialize every new Person’s visited variable to false. We also updated the toMap() method to also provide the visited variable.

Now, delete all the People in your app. Use either the UI or the Firebase console. We’ve changed our data model, so we want to make sure that all People documents have the visited field.

Go ahead and add new people. Note in the Firestore console how they all new have the visited field. Firestore with visited field

Now, we are going to add a a trailing Icon to the ListTile that indicates whether a person’s art has been visited or not. Add the following trailing property to the ListTile:

trailing: (personDocs[index].get('visited'))
    ? Icon(
        Icons.check_circle,
        color: Colors.green[700],
      )
    : const Icon(Icons.circle_outlined),

Your list should now look like this:

Person list with indicator

Each ListTile shows a circle outline because the visited field of all our documents is false. You can change one manually to true in the Firestore console if you like to see a green checkmark.

Now, let’s add code to update a Firestore document programmatically. In our example, we will make it so that if a user long-presses the list tile, that it toggles the person’s visited field.

Add the following onLongPress property to your ListTile:

onLongPress: () {
  // Get a reference to the Document you want to update.
  QueryDocumentSnapshot person = personDocs[index];
  peopleRef
      .doc(person.id)
      .update({"visited": !person.get("visited")});
},

As with deleting, we need to call peopleRef.doc(person.id) to specify which Firestore document we are updating. We then call the update method and pass to it a map of the Firestore field-value pairs we wish to update. In this case, we are simply toggling the visited field from false to true or vice versa.

You can now long-press anywhere in the ListTile to update the visited value.

The update() call is asynchronous, just like delete(). We should add a .then(...) call to the update() to provide user feedback. This is left as an exercise.

As with delete(), we do not need to manually update the list to see the changes. The update is sent to Firestore, Firestore sends new data back across the stream, and our StreamBuilder redraws the list.

Completed Code

Conclusion

We have now demonstrated all of Firestore’s basic CRUD operations. In the next lab, we will talk about how to search or query documents in the Firestore that match certain criteria.