Introduction to Cloud Firestore and CRUD operations in flutter using Firestore

Introduction to Cloud Firestore and CRUD operations in flutter using Firestore

Learn how to perform the CRUD operations in flutter using the Cloud Firestore.

Introduction

Hey everyone in this blog I am going to explain to you what is Firestore in firebase and how to perform CRUD operations in Flutter with the help of Firestore.

What is Firestore?

Firestore is a NoSQL database which is built for automatic scaling, high performance and ease of application development. The term NoSQL doesn't mean that it does not use SQL, It is a term which provides the mechanism for storage and retrieval of data in a document form rather than in a tabular format and that's how Firestore stores our data in it.

Firestore is a cloud-based database, which means our data is stored on the cloud rather than on our local machine. In Firestore we can store our data inside documents and these documents are stored inside collections. Collections are a type of container inside which we can store our data.

Creating a database inside Firestore

For creating a database inside the firebase console, Create a new project, and name it, After then it will take some time to create it and when your project is finally created follow these steps to create a database,

Firestore

Defining rules in the database

In Firestore we can define our custom rules for the user, At the time of creating the database, Firestore defines a set of custom rules for us.

The rules look like this if we've created our database in test mode,

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if
          request.time < timestamp.date(2022, 7, 31);
    }
  }
}

In the above rules, it is defined that the database will get expired after 30 days have been completed. We can define our own custom rules in this same code and publish them.

Implementing Firestore in the flutter

For implementing Firestore we mainly have to install 2 dependencies in our flutter application firebase_core to connect our flutter application to firebase & cloud_firestore to perform database operations.

Create a new collection named notes inside the cloud database which we created and firebase will tell you to include a document while creating the collection. Each document includes an ID which we can give to it or generate a random one by clicking on the Auto - ID button. I prefer to give a named ID as user_note. After giving the ID you can give the field name, the data type, and the value also. lastly, click on the Save button and your document will be saved successfully

Now that we've set up our collection and documents, Let's move to the main CRUD part

Performing CRUD operations

For performing CRUD operation in flutter we first have to create a CloudNote class which will convert the documents which we will fetch from Firestore into a Cloud Note.

class CloudNote
{
  final String title;
  CloudNote({required this.title,});  
}

Now we are creating a factory function which will convert the firebase document into a CloudNote. A factory function is just a function which will return an instance of a class.

  factory CloudNote.fromDocumentSnapshot({required DocumentSnapshot<Map<String, Dynamic>> doc}){
    return CloudNote(title: doc.data()!['title']);
  }

In the factory function, you can see we've required an instance of DocumentSnapshot which is basically a Map which takes the first argument is a String and the second one could be anything and hence it's dynamic. The document snapshot cannot guarantee the existence of your data it will either return your data and if there is no data in your document it will not return it.

Now that we've created our cloud service let's create our CRUD file in which we will define all the crud operations which we are going to perform using firebase.

Create a file named CRUD.dart under the lib folder

Here are the operations which we will include inside the CRUD.dart file,

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:crud_blog/CloudNote.dart';

class CRUD {
  final notes = FirebaseFirestore.instance.collection('notes');
  Stream<Iterable> getNotes() {
    final note = notes.snapshots().map((event) =>
        event.docs.map((e) => CloudNote.fromDocumentSnapshot(doc: e)));
    return note;
  }

  Future addNote({required String text}) async {
    await notes.add({'title': text});
  }

  Future updateNote({required String documentID, required String text}) async {
    await notes.doc(documentID).update({'title': text});
  }

  Future deleteNote({required String documentID}) async {
    await notes.doc(documentID).delete();
  }
}

In the above code, we've defined all the operations which we are going to perform using firebase and as we previously discussed firebase is a NoSQL database so every operation will not require a single query also.

Now that our CRUD operations are ready let's implement them in our app.

Note: I've implemented each and every operation using static data, which means I've provided value to them while calling it, But you can edit them as per your preferences.

Here is our main.dart file implementing all the operations

import 'package:crud_blog/CRUD.dart';
import 'package:crud_blog/CloudNote.dart';
import 'package:crud_blog/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const CRUD_operations(),
    );
  }
}

class CRUD_operations extends StatefulWidget {
  const CRUD_operations({Key? key}) : super(key: key);

  @override
  State<CRUD_operations> createState() => _CRUD_operationsState();
}

class _CRUD_operationsState extends State<CRUD_operations> {
  late final CRUD notesService;
  @override
  void initState() {
    notesService = CRUD();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('CRUD operations'),
      ),
      body: StreamBuilder(
   // StreamBuilder is a widget that builds itself based on the latest snapshot of interaction with a stream.
        stream: notesService.getNotes(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            final notes = snapshot.data as Iterable<CloudNote>;
            return ListView.builder(
              itemCount: notes.length,
              itemBuilder: (context, index) {
                final note = notes.elementAt(index);
                return ListTile(
                  title: Text(note.title),
                  tileColor: Colors.cyan,
                );
              },
            );
          } else {
            return Text('No Data found');
          }
        },
      ),
    );
  }
}

class AddUpdateDelete extends StatefulWidget {
  const AddUpdateDelete({Key? key}) : super(key: key);

  @override
  State<AddUpdateDelete> createState() => _AddUpdateDeleteState();
}

class _AddUpdateDeleteState extends State<AddUpdateDelete> {
  late final CRUD noteService;
  @override
  void initState() {
    noteService = CRUD();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Add Notes'),
      ),
      body: Column(
        children: [
          Center(
            child: TextButton(
              onPressed: () async {
                await noteService.addNote(text: 'This is Note 1');
                // we can also define custom Id, But here I've not provided becuase I've included the Id on my console
              },
              child: const Text('Add Note'),
            ),
          ),
          Center(
            child: TextButton(
              onPressed: () async {
                await noteService.updateNote(
                  documentID: 'user_note2',
                  text: 'This is updated note 2',
                );
              },
              child: const Text('Update note'),
            ),
          ),
          Center(
            child: TextButton(
              onPressed: () async {
                await noteService.deleteNote(documentID: 'user_note2');
              },
              child: const Text('Delete Note'),
            ),
          )
        ],
      ),
    );
  }
}

As you can see in the above file we've used each and every operation that we defined in our CRUD file and performed all the database operations needed.

Firestore Exceptions

As we've performed all the CRUD operations sometimes there may be some cases that your data could not get updated, deleted or added so it is a preferred way to perform all the database operations inside try catch block so you'll get a better understanding when exceptions will occur.

We can define our custom exceptions which will be thrown by the user when using firebase. So lets create a cloud_exceptions.dart file. In this file, we will define our custom exceptions using the Exception class.

// CRUD Exception using firebase

class CouldNotCreateNoteException implements Exception {}

class CouldNotGetAllNotesException implements Exception {}

class CouldNotUpdateNotesException implements Exception {}

class CouldNotDeleteNotesException implements Exception {}

Conclusion

I hope that you got a basic understanding of how to perform CRUD operations using firebase in a flutter. If you have any doubts feel free to reach me on Twitter and showwcase

Did you find this article valuable?

Support ReactPlay Blog by becoming a sponsor. Any amount is appreciated!