If you're familiar with object-oriented programming concepts, you should be able to complete this codelab. You don't need previous experience with Dart, mobile programming, or Firebase; although completing an introductory Flutter codelab first can be helpful.

Flutter and Firebase work hand-in-hand to help you build mobile apps in record time. Flutter is Google's SDK for building mobile apps for iOS and Android. Firebase gives you access to backend services for mobile applications—including authentication, storage, database, and hosting—without maintaining your own servers.

In this codelab, you'll learn how to create a Flutter app that uses Firebase. The app helps new parents choose baby names by letting friends and family vote for their favorites. Specifically, the app accesses a Cloud Firestore database, and a user action in your app (i.e., tapping a name option) updates the database using a transaction.

Here's what the final app will look like, on both iOS and Android. Yes, you read that right! With Flutter, when you build your app, you can use the same code for both iOS and Android!

Here's a short video that demonstrates building a similar app in real time. It provides a good overview of the steps you'll complete in this codelab.

What is your level of experience with building mobile apps?

Never built mobile apps Built apps for the mobile web only Built apps for Android only Built apps for iOS only Built apps for Android and iOS Built apps for mobile web, Android, and iOS

You need the following pieces of software to complete this codelab:

You can run this codelab using one or more of the following devices:

  1. Follow the Get Started: Test Drive guide to create a new Flutter app. Name the app baby_names instead of myapp. The instructions for creating a new Flutter app differ depending on your editor. If you're using an IDE, a new app is usually called a project.
  1. Make sure that your app isn't running in your emulator or device.
  2. In your IDE or editor, open the file pubspec.yaml. Add a dependency for cloud_firestore, then save the file.
dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^0.8.2     # new
  1. In your IDE (or a command line with its current directory set to your Flutter app directory), run flutter packages get.

If you get an error, make sure that the indentation in your dependencies block is exactly as shown above, using two spaces (not a tab).

  1. Using the Get Started: Test Drive page as a guide, run the default app in an emulator or on a device.

Flutter takes about a minute to build the app. The good news is that this is the last time you'll wait for compilation in this codelab—the rest of your changes will be hot-reloaded.

When your app is finished building, you should see the following app:

  1. Using your IDE or editor, open lib/main.dart. This file currently contains the entire code for the default Flutter app.
  2. Delete all of the code in main.dart, then replace it with the following:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

final dummySnapshot = [
 {"name": "Filip", "votes": 15},
 {"name": "Abraham", "votes": 14},
 {"name": "Richard", "votes": 11},
 {"name": "Ike", "votes": 10},
 {"name": "Justin", "votes": 1},
];

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Baby Names',
     home: MyHomePage(),
   );
 }
}

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() {
   return _MyHomePageState();
 }
}

class _MyHomePageState extends State<MyHomePage> {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Baby Name Votes')),
     body: _buildBody(context),
   );
 }

 Widget _buildBody(BuildContext context) {
   // TODO: get actual snapshot from Cloud Firestore
   return _buildList(context, dummySnapshot);
 }

 Widget _buildList(BuildContext context, List<Map> snapshot) {
   return ListView(
     padding: const EdgeInsets.only(top: 20.0),
     children: snapshot.map((data) => _buildListItem(context, data)).toList(),
   );
 }

 Widget _buildListItem(BuildContext context, Map data) {
   final record = Record.fromMap(data);

   return Padding(
     key: ValueKey(record.name),
     padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
     child: Container(
       decoration: BoxDecoration(
         border: Border.all(color: Colors.grey),
         borderRadius: BorderRadius.circular(5.0),
       ),
       child: ListTile(
         title: Text(record.name),
         trailing: Text(record.votes.toString()),
         onTap: () => print(record),
       ),
     ),
   );
 }
}

class Record {
 final String name;
 final int votes;
 final DocumentReference reference;

 Record.fromMap(Map<String, dynamic> map, {this.reference})
     : assert(map['name'] != null),
       assert(map['votes'] != null),
       name = map['name'],
       votes = map['votes'];

 Record.fromSnapshot(DocumentSnapshot snapshot)
     : this.fromMap(snapshot.data, reference: snapshot.reference);

 @override
 String toString() => "Record<$name:$votes>";
}
  1. Save the file, then hot-reload your app.

You should see the following app:


The app is currently just a mock. Clicking on names only prints to the console.

The next step is to connect this app to Cloud Firestore. Before doing that, you can read about how the code in main.dart is structured.

  1. If you have a Firebase account, sign in to it.

If you don't have one, you'll need to create a Firebase account. A free plan is sufficient for this codelab (and most development for most apps).

  1. In the Firebase console, click Add project.
  2. As shown in the screencap below, enter a name for your Firebase project (for example, "baby names app db"), then set the country/region to the location of your company or organization. Click Create project.
  3. After a minute or so, your Firebase project will be ready. Click Continue.

After you've created a Firebase project, you can configure one (or more) apps to use that Firebase project. All you need to do is:

If you're developing your Flutter app for both iOS and Android, you need to register both the iOS and Android versions within the same Firebase project. If you're just developing for one platform, just skip the unneeded section.

In the top-level directory of your Flutter app, there are subdirectories called ios and android. These directories hold the platform-specific configuration files for iOS and Android, respectively.

Configure iOS

  1. In the Firebase console, select Project Overview in the left nav, then click the iOS button under "Get started by adding Firebase to your app".

You'll see the dialog shown in the following screencap:

  1. The important value to provide is the iOS bundle ID, which you'll obtain using the following three steps.
  1. In the command line tool, go to the top-level directory of your Flutter app.
  2. Run the command open ios/Runner.xcworkspace to open Xcode.
  1. In Xcode, click the top-level Runner in the left pane to show the General tab in the right pane, as shown in the screencap below. Copy the Bundle Identifier value.

  1. Back in the Firebase dialog, paste the copied Bundle Identifier into the iOS bundle ID field, then click Register App.
  1. Continuing in Firebase, follow the instructions to download the config file GoogleService-Info.plist.
  2. Go back to Xcode. Notice that Runner has a subfolder also called Runner (as shown in the screencap above).
  3. Drag the GoogleService-Info.plist file (that you just downloaded) into that Runner subfolder.
  4. In the dialog that appears in Xcode, click Finish.
  5. Go back to the Firebase console. In the setup step, click Next, then skip the remaining steps and go back to the main page of the Firebase console.


You're done configuring your Flutter app for iOS!

Configure Android

  1. In the Firebase Console, select Project Overview in the left nav, then click the Android button under "Get started by adding Firebase to your app".

You'll see the dialog shown in the following screencap:

  1. The important value to provide is the Android package name, which you'll obtain using the following two steps.
  1. In your Flutter app directory, open the file android/app/src/main/AndroidManifest.xml.
  2. In the manifest element, find the string value of the package attribute. This value is the Android package name (something like com.yourcompany.yourproject). Copy this value.
  3. In the Firebase dialog, paste the copied package name into the Android package name field.
  4. (Optional) If you plan to use Google Sign In or Firebase Dynamic Links (note that these are not part of this codelab), you need to provide the Debug signing certificate SHA-1 value. Follow the instructions in the Authenticating Your Client guide to find the debug certificate fingerprint value to paste into that field.
  5. Click Register App.
  6. Continuing in Firebase, follow the instructions to download the config file google-services.json.
  7. Go to your Flutter app directory, then move the google-services.json file (that you just downloaded) into the android/app directory.
  8. Back in the Firebase console, skip the remaining steps and go back to the main page of the Firebase console.
  9. Finally, you need the Google Services Gradle plugin to read the google-services.json file that was generated by Firebase.
  10. In your IDE or editor, open android/app/build.gradle, then add the following line as the last line in the file:
apply plugin: 'com.google.gms.google-services'
  1. Open android/build.gradle, then inside the buildscript tag, add a new dependency:
buildscript {
   repositories {
       // ...
   }

   dependencies {
       // ...
       classpath 'com.google.gms:google-services:3.2.1'   // new
   }
}

You're done configuring your Flutter app for Android!

FlutterFire plugins

Your Flutter app should now be connected to Firebase.

Flutter provides access to a wide range of platform-specific services, including Firebase APIs and plugins. Plugins include platform-specific code to access services and APIs on iOS and Android.

Firebase is accessed through a number of different libraries, one for each Firebase product (for example, databases, authentication, analytics, storage). Flutter provides its own set of plugins to access each Firebase product, collectively called FlutterFire. Be sure to check the FlutterFire GitHub page for the most up-to-date list of FlutterFire plugins.

Your Firebase-Flutter setup is finished, and you're ready to start building your app!

You'll start by setting up Cloud Firestore and initializing it with some values.

  1. Open the Firebase console, then select the Firebase project that you created during setup.
  2. From the left nav Develop section, select Database.
  3. In the Cloud Firestore pane, click Create database.
  4. In the Security rules for Cloud Firestore dialog, select Start in test mode, then click Enable.

Our database will have one collection, that we'll name "baby". In the collection is where the names and votes are stored.

  1. Click Add Collection, set the collection's name to baby, then click Next.

You can now add documents to your collection. Each document has a Document ID, and we'll need to have name and votes fields (as shown in the screencap below).

  1. Enter a baby name using all lowercase letters. In this example, we used dana.
  1. For the existing Field, enter the value of name, select string for the Type, then enter the Value of Dana.
  2. Click the Add Field icon to add a second field to contain the number of votes. Select number for the Type, then initialize the Value as 0.
  3. Click Save.
  4. Add additional baby names by clicking Add Document.

After adding several documents to your collection, your database should look something like this:

Our app is now connected to Cloud Firestore!

It's time to fetch our collection (baby) and use it instead of our dummySnapshot object.

From Dart, you get the reference to Cloud Firestore by calling Firestore.instance. Specifically for our collection of baby names, call Firestore.instance.collection('baby').snapshots() to return a stream of snapshots.

Let's plug that stream of data into our Flutter UI using a StreamBuilder widget.

  1. In your IDE or editor, open lib/main.dart, then find the _buildBody method.
  2. Replace the entire method with the following code:
Widget _buildBody(BuildContext context) {
 return StreamBuilder<QuerySnapshot>(
   stream: Firestore.instance.collection('baby').snapshots(),
   builder: (context, snapshot) {
     if (!snapshot.hasData) return LinearProgressIndicator();

     return _buildList(context, snapshot.data.documents);
   },
 );
}
  1. The code that you just copy-pasted has a type error. It's trying to pass a list of DocumentSnapshot to a method that expects something else. Find _buildList and change its signature to this:
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
  ... 

Instead of a list of Map, it now takes a list of DocumentSnapshot.

  1. We're almost there. The method _buildListItem still thinks it's getting a Map. Find the start of the method, then replace it with this:
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
 final record = Record.fromSnapshot(data);

Instead of a Map, you're now taking a DocumentSnapshot, and using the Record.fromSnapshot() named constructor to build the Record.

  1. (Optional) Remove the dummySnapshot field from the top of lib/main.dart. It's not needed anymore.
  2. Save the file, then hot-reload your app.

After about a second, your app should look like this:

You've just read from the database that you created!

If you want, you can go to the Firebase console and change the database. Your app will reflect the changes almost immediately (after all Cloud Firestore is a real-time database!).

Next you will allow users to actually vote!

  1. In lib/main.dart, find the line that says onTap: () => print(record). Change it to this:
onTap: () => record.reference.updateData({'votes': record.votes + 1})

Instead of just printing the record to the console, this new line updates the baby name's database reference by incrementing the vote count by one.

  1. Save the file, then hot-reload your app.

Voting is now functional, including the update to the user interface.

How does this work? When the user taps the tile containing a name, you are telling Cloud Firestore to update the data of that reference. In turn, this causes Cloud Firestore to notify all listeners with the updated snapshot. As your app is listening through the StreamBuilder implemented above, it's updated with the new data.

It's hard to spot when testing on a single device, but our current code creates a subtle race condition. If two people with your app vote at the same time, then the value of the votes field would increase by only one -- even though two people voted for the name. This is because both apps would read the current value at the same time, add one, then write the same value back to the database. Neither user would notice anything wrong because they would both see the value of votes increase. It's extremely difficult to detect this problem through testing because triggering the error depends on doing two things inside a very small time window.

The value of votes is a shared resource, and any time that you update a shared resource (especially when the new value depends on the old value) there is a risk of creating a race condition. Instead, when updating a value in any database, you should use a transaction.

  1. In lib/main.dart, find the line that says onTap: () => record.reference.updateData({'votes': record.votes + 1}). Replace it with this:
onTap: () => Firestore.instance.runTransaction((transaction) async {
     final freshSnapshot = await transaction.get(record.reference);
     final fresh = Record.fromSnapshot(freshSnapshot);

     await transaction
         .update(record.reference, {'votes': fresh.votes + 1});
   }),
  1. Save the file, then hot-reload your app.

The voting interaction now takes a bit more time to complete. The upside, though, is that each vote counts—you removed the race condition.

How does this work? By wrapping the read and write operations in one transaction, you're telling Cloud Firestore to only commit a change if there was no external change to the underlying data while the transaction was running. If two users aren't concurrently voting on that particular name, the transaction runs exactly once. But if the number of votes changes between the transaction.get(...) and the transaction.update(...) calls, the current run isn't committed, and the transaction is retried. After 5 failed retries, the transaction fails.

Here are the final contents of lib/main.dart.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Baby Names',
     home: MyHomePage(),
   );
 }
}

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() {
   return _MyHomePageState();
 }
}

class _MyHomePageState extends State<MyHomePage> {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Baby Name Votes')),
     body: _buildBody(context),
   );
 }

 Widget _buildBody(BuildContext context) {
   return StreamBuilder<QuerySnapshot>(
     stream: Firestore.instance.collection('baby').snapshots(),
     builder: (context, snapshot) {
       if (!snapshot.hasData) return LinearProgressIndicator();

       return _buildList(context, snapshot.data.documents);
     },
   );
 }

 Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
   return ListView(
     padding: const EdgeInsets.only(top: 20.0),
     children: snapshot.map((data) => _buildListItem(context, data)).toList(),
   );
 }

 Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
   final record = Record.fromSnapshot(data);

   return Padding(
     key: ValueKey(record.name),
     padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
     child: Container(
       decoration: BoxDecoration(
         border: Border.all(color: Colors.grey),
         borderRadius: BorderRadius.circular(5.0),
       ),
       child: ListTile(
         title: Text(record.name),
         trailing: Text(record.votes.toString()),
         onTap: () => Firestore.instance.runTransaction((transaction) async {
               final freshSnapshot = await transaction.get(record.reference);
               final fresh = Record.fromSnapshot(freshSnapshot);

               await transaction
                   .update(record.reference, {'votes': fresh.votes + 1});
             }),
       ),
     ),
   );
 }
}

class Record {
 final String name;
 final int votes;
 final DocumentReference reference;

 Record.fromMap(Map<String, dynamic> map, {this.reference})
     : assert(map['name'] != null),
       assert(map['votes'] != null),
       name = map['name'],
       votes = map['votes'];

 Record.fromSnapshot(DocumentSnapshot snapshot)
     : this.fromMap(snapshot.data, reference: snapshot.reference);

 @override
 String toString() => "Record<$name:$votes>";
}

Congratulations!

You now know the basics of integrating Flutter apps with Firebase.

You've learned

Ready to share your new app with friends? Check out how you can generate platform-specific binaries for your Flutter app (an IPA file for iOS and an APK file for Android).

Additional resources

For more information on Firebase, see:

You might also find these developer resources useful as you continue working with the Flutter framework:

Here are some ways to get the latest news about Flutter:

Tell us how we're doing

How likely are you to recommend trying Flutter to your friends and colleagues? (from 1="Very unlikely" to 5="Very likely")

1 2 3 4 5