Flutter Firestore Read Data Tutorial

Cloud Firestore is a no-SQL database that allows you to store data in documents which are contained and organized by collections. In previous post, we discussed the basics of Firestore and learnt how to write data to collections.

In this article, we’ll go through the methods we can use to read data from Firestore in Flutter apps. Firebase Firestore provides several ways to retrieve data including retrieving single document, multiple documents, and real-time changes. We can retrieve single document by specifying the document ID while retrieving multiple documents require querying which we’ll cover in this tutorial. In addition, Flutter provides listeners to get real-time changes in your applications.

Before we can start our tutorial, you’ll need to create a Firebase project and set your Flutter app to enable Firebase functions. If this is you’re not sure how, take a look at the posts I linked so you can get yourself ready.

Are you ready to start our Flutter Firestore read data tutorial? Let’s GO!

Table of Contents

Introductory

First of all, let’s take a look at some of the terminology that I’ll be widely using in this tutorial. I mentioned that Firestore stores data inside documents which are organized within collections. Whenever you want to perform a CRUD operation, you will need to create a reference to the document and collection. These what we call DocumentReference and CollectionReference respectively.

Besides, when you want to read data, cloud Firestore returns either a DocumentSnapshot or QuerySnapshot.
Firestore returns a DocumentSnapshot when you access a document directly, or from a query
Firestore returns a QuerySnapshot from a collection query which gives you access to the documents within this collection. QuerySnapshot contains a list of DocumentSnapshot which you can access using the docs property. Don’t worry, we’ll talk about all this in more details in a bit!

When you want to read data from Firestore, it’s helpful to use FutureBuilder as it helps you manage the request state. FutureBuilder is a widget that builds itself based on the latest snapshot of interaction with a Future. It takes two compulsory properties which are future and builder. The future property is where you write your query while builder is a method to build the layout.

Read a single document (One-time Read)

Now, you can read a single document by calling the DocumentReference.get() method which returns a DocumentSnapshot. We’ll go through 2 examples to retrieve a document as map and as an object.

Example

In this example, we have a collection called “Users” which store various documents with users data. Each document contains 4 fields including name, email, age, and gender

firestore collection
Users Collection

Retrieve Document’s content as a map

We’ll create a FutureBuilder that retrieves the selected document as a map and displays the name of the user on the screen.

@override
Widget build(BuildContext context) {

  return FutureBuilder<DocumentSnapshot> (
    future: users.doc('0aOxrLKRfJuLoy5bJMmN').get(),
    builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
      if (snapshot.hasError) {
        return Text('Failed loading user data');
      }
      if (snapshot.hasData && !snapshot.data!.exists) {
        return Text('User data does not exist');
      }
      if (snapshot.connectionState == ConnectionState.done) {
        Map<String, dynamic> userData = snapshot.data!.data() as Map<
            String,
            dynamic>;
        return Text('Name: ${userData['name']}');
      }
      return Text('Loading user data');
    },
  );
}

Retrieve Document’s content as custom object

To use custom objects, we need to create a model class for user and write conversion functions for the class. These functions convert a map into JSON format and vice versa.  If you’re totally new to this, you can learn more about creating custom objects and its benefits.

Now, let’s create our model class and write the conversion functions

import 'package:cloud_firestore/cloud_firestore.dart';
class User {
  late String name;
  late String email;
  late  String gender;
  late int age;

  User({required this.name, required this.email, required this.gender, required this.age});

  Map<String, Object> toJson() {
    return {
      'name': name,
      'email': email,
      'gender': gender,
      'age': age
    };

  }

  factory User.fromJson(
      DocumentSnapshot<Map<String, dynamic>> snapshot,
      SnapshotOptions? options,
      ) {
    final data = snapshot.data();
    return User(
        name: data?['name'] as String,
        email: data?['email'] as String,
        gender: data?['gender'] as String,
        age: data?['age'] as int
    );
  }
}

Next, let’s write the code to retrieve data from Firestore as a custom object

@override
Widget build(BuildContext context) {

  final docRef = users.doc('0aOxrLKRfJuLoy5bJMmN').withConverter(
      fromFirestore: User.fromJson, toFirestore: (User user, _) => user.toJson());

  return FutureBuilder<DocumentSnapshot> (
    future: docRef.get(),
    builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
      if (snapshot.hasError) {
        return Text('Failed loading user data');
      }
      if (snapshot.hasData && !snapshot.data!.exists) {
        return Text('User data does not exist');
      }
      if (snapshot.connectionState == ConnectionState.done) {
        User userData = snapshot.data!.data() as User;
        return Text('Name: ${userData.name}');
      }
      return Text('Loading user data');
    },
  );
}

Reading Real-time Changes

Now, when you want to read real-time changes, you’ll need to deal with StreamBuilder. It is a widget that builds itself based on the latest snapshot of interaction with a stream. Stream is a source of asynchronous data events. Both CollectionReference and DocumentReference provide a snapshots() method which returns a stream. StreamBuilder helps automatically manage the streams state and disposal of the stream when it’s no longer used within your app.

Example

class _FireState extends State<Fire> {
  final Stream<QuerySnapshot> usersStream = FirebaseFirestore.instance
      .collection('Users').snapshots();

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: usersStream,
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
        if (snapshot.hasError) {
          return Text('Failed loading user data');
        }
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Text('Loading');
        }
        return Material(
          child: ListView(
            children: snapshot.data!.docs.map((DocumentSnapshot document) {
              Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
              return ListTile(
                title: Text(data['name']),
                subtitle: Text(data['gender']),
              );
            }).toList().cast(),

          ),
        );
      },
    );
  }

}

Firestore Querying

Firestore provides advanced capabilities for querying collections which you can use to get multiple documents from a collection. Querying works well with both one-time read and listening to real-time changes. Querying includes filtering, limiting, ordering, and start and end cursors.

1. Filtering

Firestore filtering lets you retrieve a document based on a specific field’s value. To filter documents within a collection, you can chain where() method with the collection reference and write your query inside. Within the where() method, you need to specify the filed object which you want to filter and the filter object. As you can see below, this method can take various filters.

Query<T> where(
  Object field, {
  Object? isEqualTo,
  Object? isNotEqualTo,
  Object? isLessThan,
  Object? isLessThanOrEqualTo,
  Object? isGreaterThan,
  Object? isGreaterThanOrEqualTo,
  Object? arrayContains,
  List<Object?>? arrayContainsAny,
  List<Object?>? whereIn,
  List<Object?>? whereNotIn,
  bool? isNull,
});

For example, you can use isEqualTo to get users older than 25

final users = FirebaseFirestore.instance.collection('Users');
@override
Widget build(BuildContext context) {
  return FutureBuilder<QuerySnapshot>(
    future: users.where('age', isGreaterThan: 25).get(),
    builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
      if (snapshot.hasError) {
        return Text('Failed loading user data');
      }
      if (snapshot.connectionState == ConnectionState.waiting) {
        return Text('Loading');
      }
      return Material(
        child: ListView(
          children: snapshot.data!.docs.map((DocumentSnapshot document) {
            Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
            return ListTile(
              title: Text(data['name']),
              subtitle: Text(data['gender']),
            );
          }).toList().cast(),

        ),
      );
    },
  );
}

2. Ordering

Cloud Firestore allows you to order the returned documents by a specific value. You can do this using the orderBy() method on a collection reference and specify it to descending order if you want to.

final users = FirebaseFirestore.instance.collection('Users');
@override
Widget build(BuildContext context) {
  return FutureBuilder<QuerySnapshot>(
    future: users.orderBy('age', descending: true).get(),
    builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
      if (snapshot.hasError) {
        return Text('Failed loading user data');
      }
      if (snapshot.connectionState == ConnectionState.waiting) {
        return Text('Loading');
      }
      return Material(
        child: ListView(
          children: snapshot.data!.docs.map((DocumentSnapshot document) {
            Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
            return ListTile(
              title: Text(data['name']),
              subtitle: Text(data['gender']),
            );
          }).toList().cast(),

        ),
      );
    },
  );
}

3. Limiting

You can use Firestore querying to limit the number of documents returned by a query. To do so, you can use the limit() method on a collection reference.

final users = FirebaseFirestore.instance.collection('Users');
@override
Widget build(BuildContext context) {
  return FutureBuilder<QuerySnapshot>(
    future: users.limit(2).get(),
    builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
      if (snapshot.hasError) {
        return Text('Failed loading user data');
      }
      if (snapshot.connectionState == ConnectionState.waiting) {
        return Text('Loading');
      }
      return Material(
        child: ListView(
          children: snapshot.data!.docs.map((DocumentSnapshot document) {
            Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
            return ListTile(
              title: Text(data['name']),
              subtitle: Text(data['gender']),
            );
          }).toList().cast(),

        ),
      );
    },
  );
}

In addition, you can use the limitToLast() method to limit to the last documents within the collection query. Take note that to use this method you need to at least specify 1 orderBy clause, else you’ll get an error.

final users = FirebaseFirestore.instance.collection('Users');
@override
Widget build(BuildContext context) {
  return FutureBuilder<QuerySnapshot>(
    future: users.orderBy('age').limitToLast(2).get(),
    builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
      if (snapshot.hasError) {
        return Text('Failed loading user data');
      }
      if (snapshot.connectionState == ConnectionState.waiting) {
        return Text('Loading');
      }
      return Material(
        child: ListView(
          children: snapshot.data!.docs.map((DocumentSnapshot document) {
            Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
            return ListTile(
              title: Text(data['name']),
              subtitle: Text(data['gender']),
            );
          }).toList().cast(),

        ),
      );
    },
  );
}

4. Pointing Cursors

You can set a query to start or end at a specific point within a collection by passing startAt, endAt, startAfter, or endBefore methods. Also, this requires you to specify an order clause to avoid errors

For example, you can set the query to retrieve users starting from age 30

final users = FirebaseFirestore.instance.collection('Users');

@override
Widget build(BuildContext context) {
  return FutureBuilder<QuerySnapshot>(
    future: users.orderBy('age').startAt([30]).get(),
    builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
      if (snapshot.hasError) {
        return Text('Failed loading user data');
      }
      if (snapshot.connectionState == ConnectionState.waiting) {
        return Text('Loading');
      }
      return Material(
        child: ListView(
          children: snapshot.data!.docs.map((DocumentSnapshot document) {
            Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
            return ListTile(
              title: Text(data['name']),
              subtitle: Text(data['gender']),
            );
          }).toList().cast(),

        ),
      );
    },
  );
}

Besides pointing to a field value, you can instead specify a document snapshot by passing it to startAfterDocument, startAtDocument, endAtDocument, endBeforeDocument methods.

Get all documents in a collection

You can retrieve all the documents from a collection by calling the get() method on a collection reference.

CollectionReference collection = FirebaseFirestore.instance.collection('Users').get()
.then(
(value) => //..your code goes here),
onError: (e) => print('Failed retrieving documents $e'),
);

Conclusion

This brings an end to our Flutter Firebase read Data Tutorial. We learnt how to read single documents as maps and custom objects. In addition, we learnt how to listen to real-time changes, querying on collections, and retrieving all documents in a collection.

If you wish to learn more about Firebase, I have a whole category of Firebase that’s ripe for you to explore. You can learn about Firebase Email Authentication, mobile authentication, and Facebook authentication, Writing to Firestore.

Also, don’t forget to like and share my Facebook page, share the post with those who care to learn, and subscribe to my blog to be one of the firsts to know about my newest posts!

Thank you and happy coding!

Oh hi there!
It’s nice to meet you.

Sign up to receive awesome content in your inbox, every month.

Let's Do This!

2 thoughts on “Flutter Firestore Read Data Tutorial

  1. Reading your article helped me a lot and I agree with you. But I still have some doubts, can you clarify for me? I’ll keep an eye out for your answers.

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top