Local development for your Flutter apps using the Firebase Emulator Suite

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:

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.

cd5c4753bbee8af.png 8cb4d21f656540bf.png

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

  1. Sign in to the Firebase console.
  2. In the Firebase console, click Add Project (or Create a project), and enter a name for your Firebase project (for example, "Firebase-Flutter-Codelab").

fe6aeab3b91965ed.png

  1. 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.

d1fcec48bf251eaa.png

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:

  1. In the Firebase console's Build section, click Cloud Firestore.
  2. Click Create database. 99e8429832d23fa3.png
  3. 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. 6be00e26c72ea032.png
  4. Select the location for your database (You can just use the default). Note that this location can't be changed later. 278656eefcfb0216.png
  5. 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 is a 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.

  1. Log into Firebase using your Google account by running the following command:
firebase login
  1. This command connects your local machine to Firebase and grants you access to your Firebase projects.
  1. 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
  1. 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:

  1. 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.) fe6401d769be8f53.png
  2. Next, select "Use an existing project", when prompted.

f11dcab439e6ac1e.png

  1. Now, select the project you created in a previous step: flutter-firebase-codelab.

3bdc0c6934991c25.png

  1. 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. 9bfa2d507e199c59.png
  2. 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.

8544e41037637b07.png

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.

619b7aca6dc15472.png 301c9534f594f472.png

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.

12199a85ade30459.png

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:

bb7181eb70829606.png

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.

11563f4c7216de81.png

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:

3c1bfded40733189.png

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:

791fce7dc137910a.png

Firebase console:

e0dde9aea34af050.png

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.

  1. 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.
  2. 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.
  3. 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:

  1. User fills out form and pressed Submit button
  2. The UI calls AppState.writeEntryToFirebase
  3. 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.

a978fb34fb8a83da.png

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:

f0b37f0341639035.png

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:

385d62152e99aad4.png

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:

b8a31c7a8900331.gif

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

Sparky is proud of you!

2a0ad195769368b1.gif