Cloud Firestore - Creating Data from Your App
- Objectives
- Prerequisites
- Setup - new files
- Adding data to the Firestore from Flutter
- Writing data to the Firestore programmatically
- Oh no - Security!
- Finally, successfully creating data in Firestore
- Conclusion
- Completed code
- Other Resources
Objectives
- To write Flutter code that adds data to the Firestore programmatically
Prerequisites
You must complete the Cloud Firestore - Introduction lab before this one.
Setup - new files
Put the following files in your lib/
folder:
- firestore_add.dart - a new screen for generating fake Person data for the Firestore.
- person.dart - it’s always good practice to encapsulate complex data objects, like the notion of a “Person”, in a class.
- main.dart - replace the old version. We’ve updated the main app so it forces you to LoginScreen if the user isn’t signed in, and shows a HomeScreen if they are logged in.
Adding data to the Firestore from Flutter
Adding and viewing Firestore data through the web console is excellent for debugging and setting up some initial data to test. Of course, this is not scalable, and your app will be generating data you want to share and save. So you need to be able to add it programmatically from your Flutter app.
Fortunately, there is a library for that.
We will continue with the example application from the Firebase Authentication lab. Again, you must have authentication working in your app to read/write data to the Firestore.
Adding Firestore libraries to a project
First, we need to add the Cloud Firestore libraries to our project, and then run flutterfire config
to ensure our app is configured to talk to the Firebase cloud properly.
- Open your firebase example project in VSCode.
- In the VS Code Terminal, run
flutter pub add cloud_firestore flutterfire configure flutter pub get
- Use the arrow keys to select your example project from the list, then hit Enter.
- Hit Enter to keep the platform selection.
- Select ‘yes’ for if asked to overwrite ` lib/firebase_options.dart`.
Android changes
We have heretofore ignored the android/
, ios/
, etc. folders. We need to make a change to one of these folders now. Cloud Firestore libraries do not work with old Android versions. We need to specify a minimum Android version to avoid build errors.
- Open the file
android/app/build.gradle.kts
- be careful, there are two build.gradle files. Make sure you are opening the right one. - Look for the line
minSdk flutter.minSdkVersion
- Change it to
minSdk 23
We should now be good to write Firestore code and test it on an Android emulator.
Writing data to the Firestore programmatically
Our FirestoreAddScreen is necessarily a Stateful widget. Much like with Firebase Authentication, we need to make a call to the Firestore library asynchronously, the library does the actual data transmission to the Firestore for us, and then gives us a result (which could be an error), and we have to do something with the result.
In our example app, we want to create Person data and save it to the Firestore, much like we did through the web console.
How you create the raw data is up to you. You could get it from a Form, like we did on the LoginScreen. Maybe the data is calculated and produced by some algorithm.
For this example, we don’t really care how the data is created in our app. For simplicity’s sake, I have added the static method Person.getRandomPerson()
to quickly generate a random Person
instance with data that I will put in the Firestore.
You will notice the FirestoreAddScreen has an ElevatedButton that, when pressed, invokes a method called generateAndAddPerson()
. It is in this method where we will create our data and send it to the Firestore. You can call Firestore methods from anywhere in your code, but in this case, we want to attach it to a button press.
Note following line at the top of firestore_add.dart
:
import 'package:cloud_firestore/cloud_firestore.dart';
To call the Firestore programmatically, we need a reference to the Firestore collection (“People”) we want write to or read from. Add the following class variable to _FirestoreAddScreenState
:
// This reference allows us to work with our People collection in the Firestore.
// The collection string must match the collection name in the Firestore exactly.
final peopleRef = FirebaseFirestore.instance.collection('People');
Think of the peopleRef
object as our communication path to the Firestore collection. It will handle network calls and all sorts of other things for us.
To write data to the Firestore collection, we need to call peopleRef.add(...)
. But what parameter do we give it? add()
expects an instance of the Map class. Remember how I said that you should think of Documents in Firestore as similar to a Python dictionary or a Java map? Dart also has a notion of a Map which is a set of key-value pairs like in those other languages.
Creating a new Document in the Firestore is a matter of giving add()
a map where the Map keys will be the Document’s fields, the Map values will be the Document’s fields’ values, and the Document’s field types are inferred from the Map values’ types.
Let’s add a convenience method to our Person
class to return a Map version of a Person instance’s data. Paste the following method inside your Person
class in person.dart
:
/// 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,
};
}
Aside: You may be asking yourself, why create a Person class at all? Why not just use a Map from the beginning to store data? A fair point. The main reason is that the Person object has nice named variables that can be type-checked, whereas Maps do not. The type-checking that comes with the Person class is very handy for things like auto-complete and making sure you’re referencing variables that actually exist. Embrace type checking. It helps you detect and prevent errors.
Okay, now, let’s add()
our data. Change the generateAndAddPerson()
method to the following:
void generateAndAddPerson() async {
try {
Person p = Person.getRandomPerson();
// This call transmits the data to the Firestore.
// the 'await' call blocks execution until something comes back from the network.
// the DocumentReference is a pointer to the Document in the cloud that was created.
DocumentReference doc = await peopleRef.add(p.toMap());
// Show a success message with the document ID.
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Success! Added ${doc.id}')),
);
} on FirebaseException catch (e) {
// Something went terribly wrong
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Something went wrong! ${e.message}')),
);
print(e);
}
}
There are a few things of note in this code:
- We use
await
to block the asynchronous execution until theadd()
method returns. It’s making network calls, which to a computer, are very slow. add()
returns a DocumentReference. Much likepeopleRef
is a pointer/conduit to the “People” collection in the Firestore, this DocumentReference is a pointer/conduit to the Document you just created in the Firestore.- The code is wrapped in a
try-on
block to catch exceptions that might occur. Firestore can throw exceptions if there is no network connection (Airplane mode) or the Firestore security rules prevent the user from adding data. - The result of the action is shown in a pop-up Snackbar to the user. Currently, we just display the auto-id created by the Firestore for the new document. We could easily adjust our Stateful FirestoreAddScreen to show a Text widget as well by setting a state variable, calling
setState()
, and ensuring the Text widget appears in thebuild()
method.
Oh no - Security!
When you click the button, you will get an error pop-up. You will also get an ugly exception in your Debug Console.
This is a common omission people make. When we created our Firestore database in the console, we put it in Production mode. This denies access by default. That’s a good thing actually!
Security Rules
Let’s tell Firestore we want people who are signed into our app to be able to read and write from the database.
- Click on the “Rules” tab.
- In the text area, change the contents to the following:
service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null; } } }
- Click the “Publish” button.
We are now good to go. Users logged into our app can read and write data to the Firestore, but anonymous internet strangers from other apps and websites cannot. You can read more about security rules here.
Finally, successfully creating data in Firestore
Open your Firestore database in the web console. You should now see several new entries with new data! One new entry for every time you click the button.
Try this: Have the web console visible while you press the button in your emulator or phone. You should see the new data appear in real time in the web console!
Conclusion
Congratulations! Your app is now saving data to the cloud. You now have the capacity for your app’s users to create and share data with one another because anyone using your app will be connected to your Firestore. What’s more, you could have a million users all writing data at once and the Firestore can handle it, and you do not have to change a single line of code. Of course, Google will start charging you a hefty sum for that!
I know that was a lot of setup, but the actual adding of data is only a few lines of code. The rest of the setup will make expanding on our functionality much easier.
Right now, we’re just adding data. In the next lab, we will demonstrate how to display data from the Firestore.