Advanced Git, Second Edition

Git is key to great version control and collaboration on software projects.
Stop struggling with Git and spend more time on the stuff that matters!

Home Flutter Tutorials

Firestore Tutorial for Flutter: Getting Started

In this tutorial, you’ll learn how to use Firebase Firestore databases in Flutter by creating a fun app that will help you take care of your pets.

5/5 2 Ratings

Version

  • Dart 2.13, Flutter 2.2, Android Studio 4.1
Update note: Yogesh Choudhary updated this tutorial for Flutter 2.2 and Dart 2.13. Kevin D. Moore wrote the original.

When writing mobile apps, saving the data users enter is critical. You can save the data locally, but doing so means you can’t save it permanently or share it with others. If you want to share your data, you need to save it in the cloud. One of the easiest solutions is Google’s Cloud Firestore database.

Firestore is a NoSQL-style database. Unlike traditional table-based databases that require a lot of work to insert data, with Firestore you save JSON blobs to collections. This method doesn’t impose a structure for your data and lets you change it at any time.

Of course, the downside is that you have to take that into account when reading the data. So, you need to plan how you want to store your data. Firestore offers fast, responsive data syncing without much code.

Today, you’ll build PetMedical — an app to store your pet’s medical records. The app includes a simple list of pets on the first screen and a floating action button at the bottom to add a new record.

In this tutorial, you’ll learn how to:

  • Create a new Firebase project.
  • Configure a Firestore database.
  • Use and create collections.
  • Add Firebase dependencies to your Flutter project.
  • Use streams.
  • Create models and repositories.
  • And much more!
Note: If you’re new to Flutter, please check out Getting Started With Flutter in Android Studio or Getting Started with Flutter for an overview of the basics of working with this SDK.

Getting Started

Download the starter project by using the Download Materials button at the top or bottom of the page.

This tutorial uses the latest stable version of Android Studio and the Flutter plugin. Open pubspec.yaml and click Pub get to download the latest libraries.

Open either an iPhone simulator or an Android emulator and make sure the app runs. The app should look like this:

Empty white screen with a blue Pets header and a round plus button at the bottom right corner.

The UI is in place, but you’ll need to set up Firestore and add the code to add and update records.

Creating a Firebase Account

In order to use a Firestore database, you need a Firebase account. Go to https://firebase.google.com/ and sign up for an account.

On the Welcome to Firebase page, click the Create a project button. If Firebase shows a different page, you can click Go to console in the top right corner and then click Add project.

Blue Welcome to Firebase startup page with options to Create Project or View Docs.

Now, enter the project name: PetMedical and click the Continue button.

First create a project screen with field to add a project name.

On the next page, toggle the switch for Enable Google Analytics to the off position. You won’t use analytics for this project. Then, click Create project.

Second create a project screen with toggle switch to enable or disable analytics.

You’ll see a few progress dialogs as Firebase allocates resources to your new project:

White screen with a half completed progress circle titled Provisioning Resources.

Once your project is ready, click Continue to move to the page where you’ll add Firebase to both your iOS and Android apps. Start with the iOS app.

Start up screen with option to add firebase to your app.

Registering an iOS App

To register the iOS app, click the iOS circle:

Start up screen to add firebase to your app with the iOS option circled in red.

You’ll see a dialog to register your app. Enter com.raywenderlich.petmedical for the iOS bundle ID and click the Register app button.

Note: If you created the Flutter app from scratch, enter the bundle ID you used to create the app.

Screen to register an iOS app with field to add the iOS bundle id.

Next, click the Download GoogleService-Info.plist button.

Screen showing the download option google service file for Firebase ios

Now, move this file into the iOS ‣ Runner folder. Then, from Android Studio in the Tools ‣ Flutter menu, choose Open iOS module in Xcode. In Xcode, right-click the Runner folder and choose Add files to Runner….

Next, add GoogleService-Info.plist:

Google.Service-Info.plist added directly under the Runner Folder.

Nice job! Now it’s time to register the Android app. :]

Registering an Android App

First, go back to the Firebase page. Click the Android circle to start the process of adding Firebase to Android.

Start up screen to add firebase to your app with the Android option circled in red

You’ll see a dialog to register your app. Enter com.raywenderlich.pet_medical in the Android package name field. Next, click Register app:

Screen to register an iOS app with field to add the Android package name.

Then, click the Download google-services.json button and move this file into the android ‣ app folder.

Screen showing the download option google service file for Firebase android

Now, in Android Studio, open android/build.gradle. Then, add the following dependency after the last classpath entry:

classpath 'com.google.gms:google-services:4.3.8'

Adding Google dependency in build.gradle file

Then, open android/app/build.gradle and add the following plugin after the apply from entry:

apply plugin: 'com.google.gms.google-services'

Adding Google Plugin in app/build.gradle file

Not too bad, right? :]

Initializing Firebase App

Before you use Cloud Firestore or other Firebase services, you’ll need to initialize your Firebase App. To do this, you’ll need to add the firebase_core plugin along with cloud_firestore. Open the pubspec.yaml file in your project and add the following dependencies, then click Pub get:

firebase_core: ^1.6.0
cloud_firestore: ^2.5.1

Now, open lib/main.dart and replace // TODO Initialize the Firebase App with the following code:

//1
WidgetsFlutterBinding.ensureInitialized();
//2
await Firebase.initializeApp();

Here’s what the above code does:

  1. WidgetsFlutterBinding is used to interact with the Flutter engine, which is used by Flutter plugins through platform channel to interact with the native code. Therefore, you need to call ensureInitialized to ensure the binding between widget layer and flutter engine is initialized before calling the initializeApp() method of Firebase plugin.
  2. await Firebase.initializeApp() initializes the Firebase app and then rest of the code is executed.

You’ll see an error message: Undefined name ‘Firebase’. Add the code below to the top of the file to import Firebase.

import 'package:firebase_core/firebase_core.dart';

Now you’re ready to create your Firestore database. Build and run. You’ll receive an error:

pod error during the ios build time

Oops! Looks like you’re not quite ready! You’ll fix this error in the next section.

Handling Firestore Build Error on iOS Apps

To get rid of this error, open ios/Podfile, look for the line # platform :ios, '9.0', and replace it with:

platform :ios, '10.0'

While you’re at it, you can also optimize the iOS build time by tweaking your Podfile a bit. Without this, your iOS build may take about five minutes — which can be frustrating.

At the time of writing, the Firestore iOS SDK depends on some 500,000 lines of mostly C++ code — which heavily increases the build time. To reduce the build time, you can use a pre-compiled version of the SDK by adding a line inside target 'Runner' do.

Note:Depending on which version you’re using, you might have to change the version number.
pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '8.6.0'

Editing the Podfile

Now, save the file and rebuild the app. You should see your app screen back without any errors and with an improved build time. :]

Empty white screen with a blue Pets header and a round plus button at the bottom right corner

Handling Firestore Build Error on Android Apps

Your app should build without any errors, as shown below. But, that may not always be the case with other projects.

Empty white screen with a blue Pets header and a round plus button at the bottom right corner.

Because of the large number of classes in Firestore SDK, during the build time you may get an error stating Error while merging dex archives: The number of method references in a .dex file cannot exceed 64K. If that happens, you can resolve this error by enabling multidex. To enable multiplex, follow either of the steps listed below:

  1. Changing the minSdkVersion: Open android/app/build.gradle and locate the line containing minSdkVersion. Change the version to 21 or higher.
  2. Enabling multidex explicitly for minSDKVersion 20 or lower: Open android/app/build.gradle. Under dependencies add the multidex module implementation 'com.android.support:multidex:1.0.3'. Then enable the multidex support by adding multiDexEnabled true inside defaultConfig.

Either solution is acceptable, however, if you won’t be supporting Android versions lower than 21, then feel free to go with option 1.

That’s it. Now you’re finally ready to create your Firestore database!

Creating a Firestore Database

On the Firebase console, choose the Firestore Database option under the Build menu:

Develop dropdown menu with several options including Firestore Database.

Then, click the Create database button and choose Start in test mode. This turns off any security so you can easily test your database:

Cloud Firestore start page with a Create Database button.

Create Database screen with Start in Test Mode selected.

Note:When you’re ready for production, change the setting back to production mode and add security rules.

Then, click Next. Choose a Firestore location and click Enable:

Create Databse screen with a dropdown list from which to select a Cloud Firestore location.

Nice! You created your first database.

Your screen won’t have any collections to start with:

Database screen listing no collections.

Before creating the model class, it’s time to talk about collections.

Understanding Collections

Firestore stores data in collections, which are similar to tables in a traditional database. They have a name and a list of Documents.

These Documents usually have a unique generated key (also known as document id) in the database, and they store data in key/value pairs.

These fields can have several different types:

  • String
  • Number
  • Boolean
  • Map
  • Array
  • Null
  • Timestamp
  • Geopoint

You can use Firestore’s console to manually enter data and see the data appear almost immediately in your app. If you enter data in your app, you’ll see it appear on the web and other apps almost immediately.

Now that you know about collections, it’s time to create the models for your app.

Updating the Model Classes

To retrieve your data from Firestore, you need two model classes where you’ll put the data: vaccination and pets.

The class files are already created in the starter project. You can find these classes in the lib/models directory. You’ll update the vaccination model first with the fields required for saving the data

Updating the Vaccination Model

Open vaccination.dart in the model directory and replace the class with the following:

class Vaccination {
  // 1
  final String vaccination;
  final DateTime date;
  bool? done;
  // 2
  Vaccination(this.vaccination,
      {required this.date, this.done});
  // 3
  factory Vaccination.fromJson(Map<String, dynamic> json) =>
      _vaccinationFromJson(json);
  // 4
  Map<String, dynamic> toJson() => _vaccinationToJson(this);

  @override
  String toString() => 'Vaccination<$vaccination>';
}

Here’s what the code above does:

  1. Define your fields: Name of the vaccination, date it was given and whether this vaccination is finished.
  2. Constructor for the class with parameters, where the vaccination is required and the others are optional.
  3. A factory constructor to create a vaccination from JSON.
  4. Turn this vaccination into a map of key/value pairs.

Now, add the helper functions outside the class:

// 1
Vaccination _vaccinationFromJson(Map<String, dynamic> json) {
  return Vaccination(
    json['vaccination'] as String,
    date: (json['date'] as Timestamp).toDate(),
    done: json['done'] as bool,
  );
}
// 2
Map<String, dynamic> _vaccinationToJson(Vaccination instance) =>
    <String, dynamic>{
      'vaccination': instance.vaccination,
      'date': instance.date,
      'done': instance.done,
    };

Here’s what you see:

  1. _vaccinationFromJson turns a map of values from Firestore into a vaccination class.
  2. _vaccinationToJson converts the vaccination class into a map of key/value pairs.

You need to import the Firestore library by adding this code to the top:

import 'package:cloud_firestore/cloud_firestore.dart';

Next, you’ll update the pet model.

Updating the Pet Model

Now, open pets.dart in the model directory and replace the class with the following:

class Pet {
  // 1
  String name;
  String? notes;
  String type;
  // 2
  List<Vaccination> vaccinations;
  // 3
  String? referenceId;
  // 4
  Pet(this.name,
      {this.notes, required this.type, this.referenceId, required this.vaccinations});
  // 5
  factory Pet.fromSnapshot(DocumentSnapshot snapshot) {
    final newPet = Pet.fromJson(snapshot.data() as Map<String, dynamic>);
    newPet.reference = snapshot.reference.id;
    return newPet;
  }
  // 6
  factory Pet.fromJson(Map<String, dynamic> json) => _petFromJson(json);
  // 7
  Map<String, dynamic> toJson() => _petToJson(this);

  @override
  String toString() => 'Pet<$name>';
}

Here, you have:

  1. Define your fields: Name of the pet, notes and the type of pet.
  2. List of vaccinations for this pet.
  3. A reference id to a Firestore document representing this pet.
  4. Constructor for the class with parameters, where pet name, pet type, and vaccinations are required and the others are optional.
  5. A factory constructor to create a pet from a Firestore DocumentSnapshot. You want to save the reference id for updating the doc later.
  6. A factory constructor to create a Pet from JSON.
  7. Turn this pet into a map of key/value pairs.

Next, below the pet class, add the following code:

// 1
Pet _petFromJson(Map<String, dynamic> json) {
  return Pet(json['name'] as String,
      notes: json['notes'] as String?,
      type: json['type'] as String,
      vaccinations:
          _convertVaccinations(json['vaccinations'] as List<dynamic>));
}
// 2
List<Vaccination> _convertVaccinations(List<dynamic> vaccinationMap) {
  final vaccinations = <Vaccination>[];

  for (final vaccination in vaccinationMap) {
    vaccinations.add(Vaccination.fromJson(vaccination as Map<String, dynamic>));
  }
  return vaccinations;
}
// 3
Map<String, dynamic> _petToJson(Pet instance) => <String, dynamic>{
      'name': instance.name,
      'notes': instance.notes,
      'type': instance.type,
      'vaccinations': _vaccinationList(instance.vaccinations),
    };
// 4
List<Map<String, dynamic>>? _vaccinationList(List<Vaccination>? vaccinations) {
  if (vaccinations == null) {
    return null;
  }
  final vaccinationMap = <Map<String, dynamic>>[];
  vaccinations.forEach((vaccination) {
    vaccinationMap.add(vaccination.toJson());
  });
  return vaccinationMap;
}

Here’s what this code does:

  1. Add a function to convert a map of key/value pairs into a pet.
  2. Add another function to convert a list of maps into a list of vaccinations.
  3. Convert a pet into a map of key/value pairs.
  4. Convert a list of vaccinations into a list of mapped values.

Now that you’ve updated the classes to hold your data fetched from Firestore, you need to add a way to retrieve and save it.

Creating a DataRepository Class

Next, you’ll create a DataRepository class that retrieves and saves your data. You need to isolate your use of Firebase as much as possible to follow Android best practices.

First, right-click the lib directory and select New ‣ Directory. Then name the directory repository.

Next, right-click the repository folder and choose New ‣ Dart File. Name the file data_repository and add the following:

import 'package:cloud_firestore/cloud_firestore.dart';

import '../models/pets.dart';

class DataRepository {
  // 1
  final CollectionReference collection =
      FirebaseFirestore.instance.collection('pets');
  // 2
  Stream<QuerySnapshot> getStream() {
    return collection.snapshots();
  }
  // 3
  Future<DocumentReference> addPet(Pet pet) {
    return collection.add(pet.toJson());
  }
  // 4
  void updatePet(Pet pet) async {
    await collection.doc(pet.referenceId).update(pet.toJson());
  }
  // 5
  void deletePet(Pet pet) async {
    await collection.doc(pet.referenceId).delete();
  }
}

Here’s what you added:

  1. Your top-level collection is called pets. You stored a reference to this in collection variable.
  2. Use the snapshots method to get a stream of snapshots. This listens for updates automatically.
  3. Add a new pet. This returns a future if you want to wait for the result. Note that add will automatically create a new document id for Pet.
  4. Update your pet class in the database. Similar to adding a new pet, this method uses the collection reference to update an existing pet using its ID.
  5. Delete your pet class from the database. Same as updating, except the pet is deleted using its ID.

You’ve added classes to hold, retrieve and save your data. Now you need to add a way to update your lists when users add new data.

Adding Pet List to Home Page

You’ll use StreamBuilder to fetch the list of pets on the app screen. Firestore sends streams when there is any update in the database. Here, streams are a sequence of asynchronous data that are sent when ready.

First, open home_list.dart. In _HomeListState look for the // TODO Add Data Repository and replace it with the following:

final DataRepository repository = DataRepository();

This gives access to Firestore throughout this class. Then, in the _buildHome, replace body with:

body: StreamBuilder<QuerySnapshot>(
  stream: repository.getStream(),
  builder: (context, snapshot) {
    if (!snapshot.hasData) return LinearProgressIndicator();

    return _buildList(context, snapshot.data?.docs ?? []);
  }),

The StreamBuilder first checks to see if you have any data. If not, it’ll show a progress indicator. Otherwise, it’ll call _buildList.

At this point, _buildList will throw an error: The method '_buildList' isn't defined for the type '_HomeListState'.. Get rid of this error by replacing // TODO Add _buildList with:

// 1
Widget _buildList(BuildContext context, List<DocumentSnapshot>? snapshot) {
  return ListView(
    padding: const EdgeInsets.only(top: 20.0),
    // 2
    children: snapshot.map((data) => _buildListItem(context, data)).toList(),
  );
}
// 3
Widget _buildListItem(BuildContext context, DocumentSnapshot snapshot) {
  // 4
  final pet = Pet.fromSnapshot(snapshot);

  return PetCard(pet: pet, boldStyle: boldStyle);
}

Here’s what this code does:

  1. Method to build the list of pets on the app screen.
  2. Maps the list from data, creates a new list item for each one and turns that into a list that the children parameter needs.
  3. Method that builds up individual pet cards in the ListView. It has DocumentSnapshot as a parameter
  4. Creates a Pet class from the snapshot passed in.

You’ll need to import other files by adding the below code on top of the file:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:pet_medical/pet_card.dart';
import 'package:pet_medical/repository/data_repository.dart';

Build and run. Here’s what you’ll see:

Empty white screen with a blue Pets header and a round plus button at the bottom right corner.

Great! You implemented the code to show the list of pets (when you have it).

Adding a Pet

To add a pet into the app, go to add_pet_dialog.dart and replace // TODO Add Data Repository with:

final DataRepository repository = DataRepository();

Also, replace //TODO Add New Pet to repository with:

if (petName != null && character.isNotEmpty) {
  final newPet =
      Pet(petName!, type: character, vaccinations: []);
  repository.addPet(newPet);
  Navigator.of(context).pop();
}
},

This code creates a new Pet class and uses the repository to add the new pet. At this point, you’ll see errors on first two lines. To get rid of the error in the first line, import Pet:

import 'models/pets.dart';

Resolve the error in the second line by importing DataRepository:

import 'repository/data_repository.dart';

Hot reload the app and try to add a pet by clicking on the floating action button:

adding pet into the firestore database

Nice job! Now it’s time for the Pet Detail screen.

Building the Pet Room Page

First, open pet_room.dart and replace the PetRoom constructor with:

final Pet pet;
const PetRoom({Key? key, required this.pet}) : super(key: key);

Then, import Pet and change the title to:

          
title: Text(pet.name),

Next, go to pet_card.dart and look for // TODO Add pet room navigation. Then, replace onTap with:

onTap: () => Navigator.push<Widget>(
        context,
        MaterialPageRoute(
          builder: (context) => PetRoom(pet: pet),
        ),
      ),

This will push the PetRoom screen into the app. Get rid of the import error by importing PetRoom. Build and run. Then, tap the pet card to see the changes:

navigation from the pets list to pet room

Now, it’s time to add pet details to the pet room page.

Adding Pet Details to the Pet Room Page

Open page_details.dart and replace // TODO Add data repository with:

final DataRepository repository = DataRepository();

You just created an instance of DataRepositorty in the above code. This will be used shortly for updating and deleting the data in the Firestore database. Get rid of the import error by importing DataRepository.

Next, you’ll work on the logic for updating the pet data. Find and replace // TODO Update data with:

  
if (_formKey.currentState?.validate() ?? false) {
  Navigator.of(context).pop();
  widget.pet.name = name;
  widget.pet.type = type;
  widget.pet.notes = notes ?? widget.pet.notes;

  repository.updatePet(widget.pet);
}

Now, to implement the delete method for the database. Replace // TODO Delete data with:

Navigator.of(context).pop();
repository.deletePet(widget.pet);

Finally, open pet_room.dart again and replace the body argument just below the // TODO Add pet detail with:

body: PetDetail(pet: pet),

Get rid of the import error by importing PetDetail. Now, build and run on both iOS and Android and make sure everything works.

Note: Make sure to test both iOS and Android. Don’t assume that both work without testing each of them because they have different Firestore setups.

Full app navigation

Try adding more pets and vaccination details and check the Firestore console to see what the data looks like. You can also delete the data from Firestore by tapping Delete on the pet detail page:

Delete pet data from firestore database

This is an example of some data in Firestore:

Pets collection with stored documents and fields.

Congratulations! You created both an iOS and an Android app that uses a Firestore database!

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

You can learn more about Firestore and how to store your data.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments