1. Before you begin
In this codelab, you'll learn how to use the Firebase Emulator Suite with Flutter during local development. You'll learn how to use email-password authentication via the Emulator Suite, and how to read and write data to the Firestore emulator. Finally, you'll work with importing and exporting data from the emulators, to work with the same faked data each time you return to development.
Prerequisites
This codelab assumes that you have some Flutter experience. If not, you might want to first learn the basics. The following links are helpful:
- Take a Tour of the Flutter Widget Framework
- Try the Write Your First Flutter App, part 1 codelab
You should also have some Firebase experience, but it's okay if you've never added Firebase to a Flutter project. If you're unfamiliar with the Firebase console, or you're completely new to Firebase altogether, see the following links first:
What you'll create
This codelab guides you through building a simple Journaling application. The application will have a login screen, and a screen that allows you to read past journal entries, and create new ones.
What you'll learn
You'll learn how to start using Firebase, and how to integrate and use Firebase Emulator suite into your Flutter development workflow. These Firebase topics will be covered:
Note that these topics are covered insofar as they're required to cover the Firebase emulator suite. This codelab is focused on adding a Firebase project to your Flutter app, and development using the Firebase Emulator Suite. There will not be in-depth discussions on Firebase Authentication or Firestore. If you're unfamiliar with these topics, we recommend starting with the Getting to Know Firebase for Flutter codelab.
What you'll need
- Working knowledge of Flutter, and the SDK installed
- Intellij JetBrains or VS Code text editors
- Google Chrome browser (or your other preferred development target for Flutter. Some terminal commands in this codelab will assume you're running your app on Chrome)
2. Create and set up a Firebase project
The first task you'll need to complete is creating a Firebase project in Firebase's web console. A vast majority of this codelab will focus on the Emulator Suite, which uses a locally running UI, but you have to set up a full Firebase project first.
Create a Firebase project
- Sign in to the Firebase console.
- In the Firebase console, click Add Project (or Create a project), and enter a name for your Firebase project (for example, "Firebase-Flutter-Codelab").
- Click through the project creation options. Accept the Firebase terms if prompted. Skip setting up Google Analytics, because you won't be using Analytics for this app.
To learn more about Firebase projects, see Understand Firebase projects.
The app that you're building uses two Firebase products that are available for Flutter apps:
- Firebase Authentication to allow your users to sign in to your app.
- Cloud Firestore to save structured data on the cloud and receive instant notification when data changes.
These two products need special configuration or need to be enabled using the Firebase console.
Enable Cloud Firestore
The Flutter app uses Cloud Firestore to save journal entries.
Enable Cloud Firestore:
- In the Firebase console's Build section, click Cloud Firestore.
- Click Create database.
- Select the Start in test mode option. Read the disclaimer about the security rules. Test mode ensures that you can freely write to the database during development. Click Next.
- Select the location for your database (You can just use the default). Note that this location can't be changed later.
- Click Enable.
3. Set up the Flutter app
You'll need to download the starter code, and install the Firebase CLI before we begin.
Get the starter code
Clone the GitHub repository from the command line:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Alternatively, if you have GitHub's cli tool installed:
gh repo clone flutter/codelabs flutter-codelabs
The sample code should be cloned into the flutter-codelabs
directory, which contains the code for a collection of codelabs. The code for this codelab is in flutter-codelabs/firebase-emulator-suite
.
The directory structure under flutter-codelabs/firebase-emulator-suite
consists of two Flutter projects. One is called complete
, which you can refer to if you want to skip ahead, or cross-reference your own code. The other project is called start
.
The code you want to start with is in the directory flutter-codelabs/firebase-emulator-suite/start
. Open or import that directory into your preferred IDE.
cd flutter-codelabs/firebase-emulator-suite/start
Install Firebase CLI
The Firebase CLI provides tools for managing your Firebase projects. The CLI is required to use the Emulator Suite, so you'll need to install it.
There are a variety of ways to install the CLI. The simplest way, if you're using MacOS or Linux, is to run this command from your terminal:
curl -sL https://firebase.tools | bash
After installing the CLI, you must authenticate with Firebase.
- Log into Firebase using your Google account by running the following command:
firebase login
- This command connects your local machine to Firebase and grants you access to your Firebase projects.
- Test that the CLI is properly installed and has access to your account by listing your Firebase projects. Run the following command:
firebase projects:list
- The displayed list should be the same as the Firebase projects listed in the Firebase console. You should see at least firebase-flutter-codelab.
Install the FlutterFire CLI
The FlutterFire CLI is built on top of the Firebase CLI, and it makes integrating a Firebase project with your Flutter app easier.
First, install the CLI:
dart pub global activate flutterfire_cli
Make sure the CLI was installed. Run the following command within the Flutter project directory and ensure that the CLI outputs the help menu.
flutterfire --help
Use Firebase CLI and FlutterFire CLI to add your Firebase project to your Flutter app
With the two CLIs installed, you can set up individual Firebase products (like Firestore), download the emulators, and add Firebase to your Flutter app with just a couple of terminal commands.
First, finish Firebase set up by running the following:
firebase init
This command will lead you through a series of questions needed to set up your project. These screenshots show the flow:
- When prompted to select features, select "Firestore" and "Emulators". (There is no Authentication option, as it doesn't use configuration that's modifiable from your Flutter project files.)
- Next, select "Use an existing project", when prompted.
- Now, select the project you created in a previous step: flutter-firebase-codelab.
- Next, you'll be asked a series of questions about naming files that will be generated. I suggest pressing "enter" for each question to select the default.
- Finally, you'll need to configure the emulators. Select Firestore and Authentication from the list, and then press "Enter" to each question about the specific ports to use for each emulator. You should select the default, Yes, when asked if you want to use the Emulator UI.
At the end of the process, you should see an output that looks like the following screenshot.
Important: Your output might be slightly different than mine, as seen in the screenshot below, because the final question will default to "No" if you already have the emulators downloaded.
Configure FlutterFire
Next, you can use FlutterFire to generate the needed Dart code to use Firebase in your Flutter app.
flutterfire configure
When this command is run, you'll be prompted to select which Firebase project you want to use, and which platforms you want to set up. In this codelab, the examples use Flutter Web, but you can set up your Firebase project to use all options.
The following screenshots show the prompts you'll need to answer.
This screenshot shows the output at the end of the process. If you're familiar with Firebase, you'll notice that you didn't have to create applications in the console, and the FlutterFire CLI did it for you.
Add Firebase packages to Flutter app
The final setup step is to add the relevant Firebase packages to your Flutter project. In the terminal, make sure you're in the root of the Flutter project at flutter-codelabs/firebase-emulator-suite/start
. Then, run the three following commands:
flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add cloud_firestore
These are the only packages you'll use in this application.
4. Enabling Firebase emulators
So far, the Flutter app and your Firebase project are set up to be able to use the emulators, but you still need to tell the Flutter code to reroute outgoing Firebase requests to the local ports.
First, add the Firebase initialization code and emulator setup code to the main
function in main.dart.
main.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'app_state.dart'; import 'firebase_options.dart'; import 'logged_in_view.dart'; import 'logged_out_view.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); if (kDebugMode) { try { FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080); await FirebaseAuth.instance.useAuthEmulator('localhost', 9099); } catch (e) { // ignore: avoid_print print(e); } } runApp(MyApp()); }
The first few lines of code initialize Firebase. Almost universally, if you're working with Firebase in a Flutter app, you want to start by calling WidgetsFlutterBinding.ensureInitialized
and Firebase.initializeApp
.
Following that, the code starting with the line if (kDebugMode)
tells your app to target the emulators rather than a production Firebase project. kDebugMode
ensures that targeting the emulators will only happen if you're in a development environment. Because kDebugMode
is a constant value, the Dart compiler knows to remove that code block altogether in release mode.
Start up the emulators
You should start the emulators before you start the Flutter app. First, start up the emulators by running this in the terminal:
firebase emulators:start
This command boots the emulators up, and exposes localhost ports with which we can interact with them. When you run that command, you should see output similar to this:
This output tells you which emulators are running, and where you can go to see the emulators. First, check out the emulator UI at localhost:4000
.
This is the homepage for the local emulator's UI. It lists all the emulators available, and each one is labeled with status on or off.
5. The Firebase Auth emulator
The first emulator you'll use is the Authentication emulator. Start with the Auth emulator by clicking "Go to emulator" on the Authentication card in the UI, and you'll see a page that looks like this:
This page has similarities to the Auth web console page. It has a table listing the users like the online console, and allows you manually add users. One big difference here is that the only authentication method option available on the emulators is via Email and Password. This is sufficient for local development.
Next, you will walk through the process of adding a user to the Firebase Auth emulator, and then logging that user in via the Flutter UI.
Add a user
Click the "Add user" button, and fill out the form with this information:
- Display name: Dash
- Email: dash@email.com
- Password: dashword
Submit the form, and you'll see the table now includes a user. Now you can update the code to log in with that user.
logged_out_view.dart
The only code in the LoggedOutView
widget that has to be updated is in the callback that's triggered when a user presses the login button. Update the code to look like this:
class LoggedOutView extends StatelessWidget { final AppState state; const LoggedOutView({super.key, required this.state}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Firebase Emulator Suite Codelab'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Please log in', style: Theme.of(context).textTheme.displaySmall, ), Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: () async { await state.logIn('dash@email.com', 'dashword').then((_) { if (state.user != null) { context.go('/'); } }); }, child: const Text('Log In'), ), ), ], ), ), ); } }
The updated code replaces the TODO
strings with the email and password you created in the auth emulator. And in the next line, the if(true)
line has been replaced by code that checks if state.user
is null. The code in AppClass
sheds more light on this.
app_state.dart
Two portions of the code in AppState
need to be updated. First, give the class member AppState.user the type User
from the firebase_auth
package, rather than the type Object
.
Second, fill in the AppState.login
method as shown below:
import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'entry.dart'; class AppState { AppState() { _entriesStreamController = StreamController.broadcast(onListen: () { _entriesStreamController.add([ Entry( date: '10/09/2022', text: lorem, title: '[Example] My Journal Entry', ) ]); }); } User? user; // <-- changed variable type Stream<List<Entry>> get entries => _entriesStreamController.stream; late final StreamController<List<Entry>> _entriesStreamController; Future<void> logIn(String email, String password) async { final credential = await FirebaseAuth.instance .signInWithEmailAndPassword(email: email, password: password); if (credential.user != null) { user = credential.user!; _listenForEntries(); } else { print('no user!'); } } // ... }
The type definition for user is now User?
. That User
class comes from Firebase Auth, and provides needed information such as User.displayName
, which is discussed in a bit.
This is basic code needed to log in a user with an email and password in Firebase Auth. It makes a call to FirebaseAuth to sign in, which returns a Future<UserCredential>
object. When the future completes, this code checks if there's a User
attached to the UserCredential
. If there is a user on the credential object, then a user has successfully logged in, and the AppState.user
property can be set. If there isn't, then there was an error, and it's printed.
Note that the only line of code in this method that's specific to this app (rather than general FirebaseAuth code) is the call to the _listenForEntries
method, which will be covered in the next step.
TODO: Action Icon – Reload your app, and then press the Login button when it renders. This causes the app to navigate to a page that says "Welcome Back, Person!" at the top. Authentication must be working, because it's allowed you to navigate to this page, but a minor update needs to be made to logged_in_view.dart
to display the user's actual name.
logged_in_view.dart
Change the first line in the LoggedInView.build
method:
class LoggedInView extends StatelessWidget { final AppState state; LoggedInView({super.key, required this.state}); final PageController _controller = PageController(initialPage: 1); @override Widget build(BuildContext context) { final name = state.user!.displayName ?? 'No Name'; return Scaffold( // ...
Now, this line grabs the displayName
from the User
property on the AppState
object. This displayName
was set in the emulator when you defined your first user. Your app should now display "Welcome back, Dash!" when you log in, rather than TODO
.
6. Read and Write data to Firestore emulator
First, check out the Firestore emulator. On the Emulator UI homepage (localhost:4000
), click "Go to emulator" on the Firestore card. It should look like this:
Emulator:
Firebase console:
If you have any experience with Firestore, you'll notice that this page looks similar to the Firebase console Firestore page. There are a few notable differences, though.
- You can clear all data with the tap of one button. This would be dangerous with production data, but is helpful for rapid iteration! If you're working on a new project and your data model changes, it's easy to clear out.
- There is a "Requests" tab. This tab allows you to watch incoming requests made to this emulator. I will discuss this tab in more detail in a bit.
- There are no tabs for Rules, Indexes or Usage. There is a tool (discussed in the next section) that helps write security rules, but you cannot set security rules for the local emulator.
To sum that list up, this version of Firestore provides more tools useful during development, and removes tools that are needed in production.
Write to Firestore
Before discussing the ‘Requests' tab in the emulator, first make a request. This requires code updates. Start by wiring up the form in the app to write a new journal Entry
to Firestore.
The high-level flow to submit an Entry
is:
- User fills out form and pressed
Submit
button - The UI calls
AppState.writeEntryToFirebase
AppState.writeEntryToFirebase
adds an entry to Firebase
None of the code involved in step 1 or 2 needs to change. The only code that needs to be added for step 3 will be added in the AppState
class. Make the following change to AppState.writeEntryToFirebase
.
app_state.dart
import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'entry.dart'; class AppState { AppState() { _entriesStreamController = StreamController.broadcast(onListen: () { _entriesStreamController.add([ Entry( date: '10/09/2022', text: lorem, title: '[Example] My Journal Entry', ) ]); }); } User? user; Stream<List<Entry>> get entries => _entriesStreamController.stream; late final StreamController<List<Entry>> _entriesStreamController; Future<void> logIn(String email, String password) async { final credential = await FirebaseAuth.instance .signInWithEmailAndPassword(email: email, password: password); if (credential.user != null) { user = credential.user!; _listenForEntries(); } else { print('no user!'); } } void writeEntryToFirebase(Entry entry) { FirebaseFirestore.instance.collection('Entries').add(<String, String>{ 'title': entry.title, 'date': entry.date.toString(), 'text': entry.text, }); } // ... }
The code in the writeEntryToFirebase method grabs a reference to the collection called "Entries" in Firestore. It then adds a new entry, which needs to be of type Map<String, String>
.
In this case, the "Entries" collection in Firestore didn't exist, so Firestore created one.
With that code added, hot reload or restart your app, log in, and navigate to the EntryForm
view. You can fill in the form with whatever Strings
you'd like. (The Date field will take any String, as it's been simplified for this codelab. It doesn't have strong validation or care about DateTime
objects in any way.)
Press submit on the form. Nothing will happen in the app, but you can see your new entry in the emulator UI.
The requests tab in the Firestore emulator
In the UI, navigate to the Firestore emulator, and look at the "Data" tab. You should see that there's now a Collection at the root of your database called "Entries". That should have a document which contains the same information you entered into the form.
That confirms that the AppState.writeEntryToFirestore
worked, and now you can further explore the request in the Requests tab. Click that tab now.
Firestore emulator requests
Here, you should see a list that looks similar to this:
You can click into any of those list items and see quite a bit of helpful information. Click on the CREATE
list item that corresponds to your request to create a new journal entry. You'll see a new table that looks like this:
As mentioned, the Firestore emulator provides tools to develop your app's security rules. This view shows exactly what line in your security rules this request passed (or failed, if that was the case). In a more robust app, Security Rules can grow and have multiple authorization checks. This view is used to help write and debug those authorization rules.
It also provides an easy way to inspect every piece of this request, including the metadata and the authentication data. This data is used to write complex authorization rules.
Reading from Firestore
Firestore uses data synchronization to push updated data to connected devices. In Flutter code, you can listen (or subscribe) to Firestore collections and documents, and your code will be notified any time data changes. In this app, listening for Firestore updates is done in the method called AppState._listenForEntries
.
This code works in conjunction with the StreamController
and Stream
called AppState._entriesStreamController
and AppState.entries
, respectively. That code is already written, as is all the code needed in the UI to display the data from Firestore.
Update the _listenForEntries
method to match the code below:
app_state.dart
import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'entry.dart'; class AppState { AppState() { _entriesStreamController = StreamController.broadcast(onListen: () { _entriesStreamController.add([ Entry( date: '10/09/2022', text: lorem, title: '[Example] My Journal Entry', ) ]); }); } User? user; Stream<List<Entry>> get entries => _entriesStreamController.stream; late final StreamController<List<Entry>> _entriesStreamController; Future<void> logIn(String email, String password) async { final credential = await FirebaseAuth.instance .signInWithEmailAndPassword(email: email, password: password); if (credential.user != null) { user = credential.user!; _listenForEntries(); } else { print('no user!'); } } void writeEntryToFirebase(Entry entry) { FirebaseFirestore.instance.collection('Entries').add(<String, String>{ 'title': entry.title, 'date': entry.date.toString(), 'text': entry.text, }); } void _listenForEntries() { FirebaseFirestore.instance .collection('Entries') .snapshots() .listen((event) { final entries = event.docs.map((doc) { final data = doc.data(); return Entry( date: data['date'] as String, text: data['text'] as String, title: data['title'] as String, ); }).toList(); _entriesStreamController.add(entries); }); } // ... }
This code listens to the "Entries" collection in Firestore. When Firestore notifies this client that there is new data, it passes that data and the code in _listenForEntries
changes all of its child documents into an object our app can use (Entry
). Then, it adds those entries to the StreamController
called _entriesStreamController
(which the UI is listening to). This code is the only update required.
Finally, recall that the AppState.logIn
method makes a call to _listenForEntries
, which begins the listening process after a user has logged in.
// ... Future<void> logIn(String email, String password) async { final credential = await FirebaseAuth.instance .signInWithEmailAndPassword(email: email, password: password); if (credential.user != null) { user = credential.user!; _listenForEntries(); } else { print('no user!'); } } // ...
Now run the app. It should look like this:
7. Export and import data into emulator
Firebase emulators support importing and exporting data. Using the imports and exports allows you to continue development with the same data when you take a break from development and then resume. You can also commit data files to git, and other developers you're working with will have the same data to work with.
Export emulator data
First, export the emulator data you already have. While the emulators are still running, open a new terminal window, and enter the following command:
firebase emulators:export ./emulators_data
.emulators_data
is an argument, which tells Firebase where to export the data. If the directory doesn't exist, it is created. You can use any name you'd like for that directory.
When you run this command, you'll see this output in the terminal where you ran the command:
i Found running emulator hub for project flutter-firebase-codelab-d6b79 at http://localhost:4400 i Creating export directory /Users/ewindmill/Repos/codelabs/firebase-emulator-suite/complete/emulators_data i Exporting data to: /Users/ewindmill/Repos/codelabs/firebase-emulator-suite/complete/emulators_data ✔ Export complete
And if you switch to the terminal window where the emulators are running, you'll see this output:
i emulators: Received export request. Exporting data to /Users/ewindmill/Repos/codelabs/firebase-emulator-suite/complete/emulators_data. ✔ emulators: Export complete.
And finally, if you look in your project directory, you should see a directory called ./emulators_data
, which contains JSON
files, among other metadata files, with the data you've saved.
Import emulator data
Now, you can import that data as part of your development workflow, and start where you left off.
First, stop the emulators if they're running by pressing CTRL+C
in your terminal.
Next, run the emulators:start
command that you've already seen, but with a flag telling it what data to import:
firebase emulators:start --import ./emulators_data
When the emulators are up, navigate to the emulator UI at localhost:4000
, and you should see the same data you were previously working with.
Export data automatically when closing emulators
You can also export data automatically when you quit the emulators, rather than remembering to export the data at the end of every development session.
When you start your emulators, run the emulators:start
command with two additional flags.
firebase emulators:start --import ./emulators_data --export-on-exit
Voila! Your data will now be saved and reloaded each time you work with the emulators for this project. You can also specify a different directory as an argument to the –export-on-exit flag
, but it will default to the directory passed to –import
.
You can use any combination of these options as well. This is the note from the docs: The export directory can be specified with this flag: firebase emulators:start --export-on-exit=./saved-data
. If --import
is used, the export path defaults to the same; for example: firebase emulators:start --import=./data-path --export-on-exit
. Lastly, if desired, pass different directory paths to the --import
and --export-on-exit
flags.
8. Congratulations!
You have completed Get up and running with Firebase emulator and Flutter. You can find the completed code for this Codelab in the "complete" directory on github: Flutter Codelabs
What we've covered
- Setting up a Flutter app to use Firebase
- Setting up a Firebase project
- FlutterFire CLI
- Firebase CLI
- Firebase Authentication emulator
- Firebase Firestore emulator
- Importing and exporting emulator data
Next Steps
- Learn more about using Firestore and Authentication in Flutter: Get to know Firebase for Flutter Codelab
- Explore other Firebase tools that offer emulators:
- Cloud Storage
- Cloud Functions
- Realtime Database
- Explore FlutterFire UI to quickly add Google Authentication to your app.
Learn more
- Firebase site: firebase.google.com
- Flutter site: flutter.dev
- FlutterFire Firebase Flutter widgets: firebase.flutter.dev
- Firebase YouTube channel
- Flutter YouTube channel
Sparky is proud of you!