Cloud Firestore - Reading, Updating, and Deleting Data
- Objectives
- Prerequisites
- Reading and Displaying Data from the Cloud Firestore
- Using a StreamBuilder to create a List that updates in realtime
- Deleting documents
- Updating Firestore data
- Completed Code
- Conclusion
Objectives
- to display a Collection from the Firestore as a list
- to stream data changes in the Firestore in real time
- to delete a Firestore document
- to update a Firestore document
Prerequisites
You must complete Cloud Firestore: Introduction and Adding Data prior to this lab.
Reading and Displaying Data from the Cloud Firestore
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.
Let’s create a List that displays all the documents in our People
Firestore collection we created in the previous lab. We are going to create a custom StatelessWidget for this and include it in our FirestoreAddScreen.
Open firestore_add.dart
. Create a blank Stateless widget called PeopleList
:
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 have a button at the top and a large 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:
stream
: this is the stream of data we need.peopleRef.snapshots()
will provide this.builder
: an anonymous function that says what to do with each snapshot. The term snapshot represents a stream’s data at a point in time. The stream will yield a new snapshot each time the data in the Cloud Firestore changes. We need to parse the contents of the snapshot and build widgets from it.
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 an 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:
personDocs[index]
gives us a Document that can be accessed like a map or dictionary.personDocs[index].get('first')
says to get the value of the field namedfirst
from the document. This is an example of how you get individual fields from a Firestore document programmatically.
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:
- User taps a list item to indicate it is deleted
- Our app tells Firestore to delete the item
- Firestore sends updates to the
peopleRef.snapshots()
stream - 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:
person
is aQueryDocumentSnapshot
. A QueryDocumentSnapshot is a reference to an individual Document from a snapshot, e.g., the document representing “Raphael Santi”.person.id
is referencing the unique identifier of the QueryDocumentSnapshot. Thisid
property is the id of the Document in the Collection when you look in your Firestore console.peopleRef.doc(person.id)
gives a DocumentReference to the specific Document in the People collection corresponding to theid
value. This DocumentReference is immensely useful because it lets us delete or update a specific existing document in the Firestore.peopleRef.doc(person.id).delete()
is the call that tells Firestore to delete the specific Document.delete()
is an asynchronous call. We could end here and be done. However, we should provide feedback once deletion is completed. Remember, every user action should provide feedback..then(...)
this specifies what to do afterdelete()
finishes.- The first argument is an anonymous function that executes when
delete()
finishes normally. In our case, we’re just going to the item that was deleted. - The second argument,
onError
, is an anonymous function that executes if an Exception occurred while deleting from Firestore, which may happen if the network drops out or the current user doesn’t have permission to delete the document. - We should really make both of these Snackbar’s or some other form of UI feedback so the user can see the results of deletion.
- The first argument is an anonymous function that executes when
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.
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:
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.