DEV Community

Tom Cohen
Tom Cohen

Posted on • Edited on

(Flutter) CRUD operations with Firestore database.

Introduction

While building my app I found 100 ways to call data from the firestorm database, especially that there's still some outdated information in the internet.
In this post I hope to give the clearest way to achieve all the crud operations especial the READ which for me took many tries until I got it right.

I'm assume already you set up the firebase and familiar with Flutter's syntax.

Setup the model
First I suggest you create the collection and a "demo" document inside Firestone just to grasp how the model we'll create after will look.

Image description

like you can see iv created the collection animals and inside there are a couple of fields. One particularly field is the "id" field, personally I like to create each document with the uniq id firebase automatically generates for it, this helps to avoid clashing. I create the field "id" so accessing the id would be easy, there are other ways to access it by calling the document id in the database but we'll not go over that in here. there are times that giving other types of id are preferable, for example when I create a "users" collection I prefer to keep the id's as their emails, this helps searching for the users especially if you group them later on in other collections.

Back to our collection "animals", now that I know what I what the object in my database to look like I'll create the model in flutter.

for that well create a new file animals_model.dart.
Inside well create a class for animals and list the requirements to create an "animals" object:

class Animal {
  String id; //id of the document inside firestore's db
  String name; // name of the animal
  int nubmer_of_legs; // nubmer of legs the animals has
  DateTime creation_date; // when the document was added tot he db

  Animal ({
    this.id = '',
    required this.name,
    required this.nubmer_of_legs,
    required this.creation_date,
  });}
Enter fullscreen mode Exit fullscreen mode

This class will hold a model for object. the model will define all the necessary data that would be saved into Firestore.
So now we can already create an object of type "animals" but we need to add it to the database. for this will make the createAnimalDoc method, but before that we need to be-able to convert the object data to json and back from json.

  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
        'nubmer_of_legs': nubmer_of_legs,
        'creation_date': creation_date,
      };

  static Animal fromJson(Map<String, dynamic> json) => Animal(
        id: json['id'],
        name: json['name'],
        nubmer_of_legs: json['nubmer_of_legs'],
        creation_date: (json['creation_date'] as Timestamp).toDate(), // import 'package:cloud_firestore/cloud_firestore.dart';
      );
Enter fullscreen mode Exit fullscreen mode

the toJson() takes the animal object data and puts it into json format so Firestone can understand, the fromJson maps all the fields and gets the data and places it into the model builder variable so we can access it in our code.

CREATE
after all this setup we can finally start the CRUD operations and well start with create.
to create a new document into firebase we simply create the method createAnimalDoc like the following:

  Future createAnimalDoc(String name, int nubmer_of_legs, DateTime creation_date) async {
    final newDocAnimal = FirebaseFirestore.instance.collection('groups').doc();
    final animal = Animal(
        name: name,
        id: newDocAnimal.id,
        nubmer_of_legs: nubmer_of_legs,
        creation_date: creation_date);
    await newDocAnimal.set(animal.toJson());
  }
Enter fullscreen mode Exit fullscreen mode

In here we put all the variables into the function but if you use controllers you can also list them here if you used a form of some sort.
if i were to create a new document with a specific id would enter it inside the ...doc('ENTER ID');

READ
In my opinion read is the most versatile of the crud operations with flutter+firestore. this is because of all the different data you might request from the database. You obviously don't won't to call all the data you have but only whats relevant to your use case. ill list a few examples and how to solve/approach them in your code.
before we start there's one thing to remember, to display that data into the page in flutter you must use the widgets FutureBuilder() or StreamBuilder(). Essentially you need to request the data and wait for in to arrive, those widgets are what can do that while giving you the ability to check if the data exists, or if there's an error or if it arrived and what to do with it.
the difference between FutureBuilder() and StreamBuilder() is that FutureBuilder() requests snapshot of the data once when the widget is called, waits for it and then displays it. the problem with it is if the data were to change it wont know it until it is called again (if you refresh the page or whatever). StreamBuilder() on the other hand requests a stream and continuously pips information to the page. so if the data changes in the database it will update inside the page too.
What to use is judged by what you need. if one snapshot is enough and you dont expect the information to change then use FutureBuilder() as its lighter on the app and makes less calls to the database. if the information need to be updated real-time then StreamBuilder() is your guy.

lets see how to use is in our code, in this example ill use a StreamBuidler():
first ill create an empty page

import 'package:flutter/material.dart';

class AnimalsPage extends StatefulWidget {
  const AnimalsPage({super.key});

  @override
  State<AnimalsPage> createState() => _AnimalsPageState();
}

class _AnimalsPageState extends State<AnimalsPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

now ill wrtie the call method that makes the 'pipe' to firestore:

    Stream<List<Animal>> fetchAnimals() {
    return FirebaseFirestore.instance
        .collection('animals')
        .snapshots()
        .map((snapshot) => snapshot.docs
            .map((doc) => Animal.fromJson(doc.data()))
            .toList());
  }
Enter fullscreen mode Exit fullscreen mode

now ill set up StreamBuilder()

  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: StreamBuilder<List<Animal>>(
          stream: fetchAnimals(), // feed our stream to the stream builder
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              final animals = snapshot.data!;
              return ListView.builder(
                itemCount: animals.length,
                itemBuilder: (context, index) {
                  return animalTile(animals[index]);
                }
                );
            } else if (snapshot.hasError) {
              return Text('Something went wrong!');
            } else {
              return Text('Something went wrong!');
            }
          },
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

as you can see if check if the snapshot's data exists, this is impotent for debugging.
and lastly we need to build the tile that will display the information. i like to do it with a separate widget in the same file:

Widget animalTile(Animal animal) => ListTile(
        title: Container(
          height: 50,
          width: 50,
          child: Text(animal.name),
        ),
      );
Enter fullscreen mode Exit fullscreen mode

now we can request any information from the database regarding the collection animals.

Ok so how do we do it with a futureBuilder()? same way essentially:

  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: FutureBuilder<List<Animal>>(
          future: fetchAnimals(),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              final animals = snapshot.data!;
              return ListView.builder(
                  itemCount: animals.length,
                  itemBuilder: (context, index) {
                    return animalTile(animals[index]);
                  });
            } else if (snapshot.hasError) {
              return Text('Something went wrong!');
            } else {
              return Text('Something went wrong!');
            }
          },
        ),
      ),
    );
  }

  Future<List<Animal>> fetchAnimals() async {
    var collectionSnapshot =
        await FirebaseFirestore.instance.collection('animals').get();

    return collectionSnapshot.docs.map((doc) => Animal.fromJson(doc.data())).toList();
  }
Enter fullscreen mode Exit fullscreen mode

the method fetchAnimals() needs to be asynchronous to wait for the snapshot to arrive from the database.

SPECIAL CASES:

  • when you want to get specific document from the whole collection you can call .collection.doc(DOCUMENT_ID)

  • if you want to search for documents that have a specific field you can use the .where() to achieve this, more information in the official documentaion.

Update
This one is pretty simple, you get in the instance of the document and then just call .update()
example:

FirebaseFirestore.instance.collection('animals').doc('DOC_ID').update({
'FIELD1': 'NEW_DATA',
'FIELD2': 'NEW_DATA',
});

Delete
Same with the update, you get in the instance of the document and then just call .delete()
example:
FirebaseFirestore.instance.collection('animals').doc('DOC_ID').delete();

hopefully this give more current up to date information about the subject, keep in mind there are other ways to approach the CRUD functions with firestore this is just my way of doing it.

-T

Top comments (0)