Flutter : How to do CRUD with Firebase RTDB

Introduction

In previous post of Flutter : How to do user login with Firebase, we talked about how to implement user login or sign up screen with Firebase authentication. Using the same project, we are going to showcase CRUD or create, read, update and delete operations with Firebase RTDB or real time database in this post.

Getting Started

This project requires you to register your project with Firebase and include the downloaded config file into your project. You can get the steps needed in the previous post mentioned above. This step is only needed if you prefer to setup own Firebase database, else you are free to use mine with the config file I have also included in the github project. Do find the link to the project at the bottom of this post.

👉Step 1: Create model class

So back to where we have left previously, we managed to display Welcome message after user successfully logs into their account. This is shown inside home_page.dart . To keep our to-do application simple, we are just going to store the name of to-do and enable user to mark as completed or not. To store information about each to-do item, we need to have a model class. A model class for a to-do item would look like this:

/models/todo.dart

class Todo {
  String key;
  String subject;
  bool completed;
  String userId;

  Todo(this.subject, this.userId, this.completed);

  Todo.fromSnapshot(DataSnapshot snapshot) :
    key = snapshot.key,
    userId = snapshot.value["userId"],
    subject = snapshot.value["subject"],
    completed = snapshot.value["completed"];

  toJson() {
return {
      "userId": userId,
      "subject": subject,
      "completed": completed,
    };
  }
}

Each to-do item is unique and has its own key . Each item has a name or subject , a flag to keep track of its completion or completed and userId of who created this item. To create a new todo, all parameters except key is required to pass into the constructor Todo() . The key is automatically generated by RTDB and stored when new todo is added.

When data is fetched from Firebase RTDB, it is in json format. Hence we have the Todo.fromSnapshot(DataSnapshot snapshot) allows us to map data from json format to Todo format. The toJson() does the opposite which is to map the data back into json format before we upload into Firebase RTDB.

👉Step 2: Initialize query

Back in our home_page.dart , we created a list of todos by using List<Todo> _todoList = new List() . When a list of todos is fetched from Firebase, we will store it into local list variables.

We use final FirebaseDatabase _database = FirebaseDatabase.instance; to get access to the Firebase instance. We then build a query from this instance using :

Query _todoQuery = _database
    .reference()
    .child("todo")
    .orderByChild("userId")
    .equalTo(widget.userId);

In this query, using the FirebaseDatabase instance, we retrieve a reference to all the data under path /todo . If you have another level in todo, then your query would be _database.reference().child("todo").child("another level") . Both .orderByChild("xx xx") and .equalTo("xx xx") is what I use to tell Firebase I want a list of todos where each todo’s userId is the one I give to you. Makes sense?

Here is how it looks like in the RTDB:

👉Step 3: Setup listeners

Using the query that we just build above, we are going to attach 2 kinds of stream subscriptions to it. One is onChildAdded and another is onChildChanged . What onChildAdded.listen() does is it listens for any new todo item added into Firebase and receives an event and pass into callback function which in this case is the _onEntryAdded . Same goes for onChildChanged.listen() , which listens for any change of data in the Firebase such as mark todo item as done.

_onTodoAddedSubscription = _todoQuery.onChildAdded.listen(_onEntryAdded);
_onTodoChangedSubscription = _todoQuery.onChildChanged.listen(_onEntryChanged);

So what is the function of the _onEntryAdded ? It catches the event snapshot and converts from json to todo model format and adds to the list of todos.

_onEntryAdded(Event event) {
  setState(() {
    _todoList.add(Todo.fromSnapshot(event.snapshot));
  });
}

For _onEntryChanged function, it retrieves the key from the event snapshot and get the index from the list of todo. Then from the list index, it updates that particular todo with the one from event snapshot.

_onEntryChanged(Event event) {
var oldEntry = _todoList.singleWhere((entry) {
return entry.key == event.snapshot.key;
  });

  setState(() {
    _todoList[_todoList.indexOf(oldEntry)] = Todo.fromSnapshot(event.snapshot);
  });
}

To properly unsubscribe to the StreamSubscription , we simply use .cancel() inside the dispose() method

@override
void dispose() {
  _onTodoAddedSubscription.cancel();
  _onTodoChangedSubscription.cancel();
super.dispose();
}

👉Step 4: Build that ListView

I like to use ListView when needed to iterate over a list of items which is dynamically changing in size and show them in a list. So in this case, we are going to iterate over each todo items in _todoList . ListView takes in itemCount which is simply the size of the todo list, i.e, _todoList.count . ListView also takes in itemBuilder which is the part that will build the single tile to display a single todo item. We are going to use ListTile widget to display a single todo item. ListTile accepts some parameters such as trailing for putting icon or other widget at the right side of ListTile , title and subtitle for display text of 2 different context and sizes, leading similar to trailing but for the left side of the ListTile and others.

On each ListTile , we are going to display a grey tick if the todo is not completed and green tick if the todo is completed. For this, we can use the ternary operator which is ? , similar to an if-else statement.

To use it, we provide a bool check on certain condition (in this case is checking the completed flag for an item in Firebase) and end it with ?

(_todoList[index].completed) ? [Do something if completed] : [Do something if not completed]

Hence, our ListTile looks like this:

child: ListTile(
  title: Text(
    subject,
    style: TextStyle(fontSize: 20.0),
  ),
  trailing: IconButton(
      icon: (_todoList[index].completed)
          ? Icon(
        Icons.done_outline,
        color: Colors.green,
        size: 20.0,
      )
          : Icon(Icons.done, color: Colors.grey, size: 20.0),
      onPressed: () {
        _updateTodo(_todoList[index]);
      }),
)

And overall ListView :

Widget _showTodoList() {
if (_todoList.length > 0) {
return ListView.builder(
        shrinkWrap: true,
        itemCount: _todoList.length,
        itemBuilder: (BuildContext context, int index) {
          String todoId = _todoList[index].key;
          String subject = _todoList[index].subject;
          bool completed = _todoList[index].completed;
          String userId = _todoList[index].userId;
return Dismissible(
            key: Key(todoId),
            background: Container(color: Colors.red),
            onDismissed: (direction) async {
              _deleteTodo(todoId, index);
            },
            child: ListTile(
              title: Text(
                subject,
                style: TextStyle(fontSize: 20.0),
              ),
              trailing: IconButton(
                  icon: (completed)
                      ? Icon(
                    Icons.done_outline,
                    color: Colors.green,
                    size: 20.0,
                  )
                      : Icon(Icons.done, color: Colors.grey, size: 20.0),
                  onPressed: () {
                    _updateTodo(_todoList[index]);
                  }),
            ),
          );
        });
  } else {
return Center(child: Text("Welcome. Your list is empty",
      textAlign: TextAlign.center,
      style: TextStyle(fontSize: 30.0),));
  }
}

Notice the ListTile is wrapped with another widget call Dismissible . This is a widget that allows user to swipe the entire ListTile to mimic the action swipe to delete.

👉Step 5: Fabulous FAB

Still at the home_page.dart, in the build method that returns a Scaffold , below body , we are going to create a FAB or floating action button. The purpose for this button is to allow user to add new todo into the list. The FAB will show an alert dialog containing a text field for user to input name of new todo.

floatingActionButton: FloatingActionButton(
  onPressed: () {
    _showDialog(context);
  },
  tooltip: 'Increment',
  child: Icon(Icons.add),
)

For the alert dialog to show up, you cannot just return a AlertDialog and expect it to show. Instead we need to use await showDialog() and return AlertDialog inside this builder. The AlertDialog will house a textfield which its value will be held by a textEditingController with 2 FlatButtons of save and cancel. The save button will obviously get the new todo item name and create a new todo instance before uploading into the Firebase.

_showDialog(BuildContext context) async {
  _textEditingController.clear();
await showDialog<String>(
      context: context,
      builder: (BuildContext context) {
return AlertDialog(
          content: new Row(
            children: <Widget>[
new Expanded(
                  child: new TextField(
                controller: _textEditingController,
                autofocus: true,
                decoration: new InputDecoration(
                  labelText: 'Add new todo',
                ),
              ))
            ],
          ),
          actions: <Widget>[
new FlatButton(
                child: const Text('Cancel'),
                onPressed: () {
                  Navigator.pop(context);
                }),
new FlatButton(
                child: const Text('Save'),
                onPressed: () {
                  _addNewTodo(_textEditingController.text.toString());
                  Navigator.pop(context);
                })
          ],
        );
      });
}

👉Step 5: Let’s CRUD

👉Create

For create a new todo item, we will take the name input by user at the TextField inside AlertDialog when they tapped on FloatingActionButton . We instantiate a new todo object with the name input. Finally we upload to Firebase using _database.reference().child(“todo”).push().set(todo.toJson())

_addNewTodo(String todoItem) {
if (todoItem.length > 0) {
    Todo todo = new Todo(todoItem.toString(), widget.userId, false);
    _database.reference().child("todo").push().set(todo.toJson());
  }
}

👉Read

For read, it has been mentioned above that you will need to build a query which is :

_todoQuery = _database
    .reference()
    .child("todo")
    .orderByChild("userId")
    .equalTo(widget.userId);

From the query, we will attach 2 listeners which is onChildAdded and onChildChanged which will trigger each respective callback methods with event snapshots. From the event snapshot, we simply convert them into todo class and add to list

_onEntryAdded(Event event) {
  setState(() {
    _todoList.add(Todo.fromSnapshot(event.snapshot));
  });
}
_onEntryChanged(Event event) {
var oldEntry = _todoList.singleWhere((entry) {
return entry.key == event.snapshot.key;
  });

  setState(() {
    _todoList[_todoList.indexOf(oldEntry)] =
        Todo.fromSnapshot(event.snapshot);
  });
}

To help improve on querying based on userId, it is recommend to set a rule in Firebase RTDB rules. This is call indexing and helps Firebase to optimize your data arrangement improve response time. For more information, refer to Index Your Data by Firebase.

{
  /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
  "rules": {
    "todo": {
      ".indexOn": "userId",
    },
    ".read": true,
    ".write": true
  }
}

👉Update

User can mark each todo item as completed or undo this step. They can simply tap on the tick icon on the right side of each todoListTile . For the update, we need to get the todo.key as we need to access to path /todo/todo-unique-key to be able to update what is in that path. The method is similar to Create in the sense it is using .set() but the difference is the addition of .child(todo.key) in the path.

_updateTodo(Todo todo) {
  //Toggle completed
  todo.completed = !todo.completed;
if (todo != null) {
    _database.reference().child("todo").child(todo.key).set(todo.toJson());
  }
}

👉Delete

Deleting item from Firebase is straightforward. Similar to Update, we need to get the correct todo.key but we will use method .remove() .

Do note that there is no listener for item removal unlike listener to item added or changed. Hence there is no way those 2 listeners method will be triggered and get the latest snapshot of the database. For this, we need to manually remove the item from our local _todoList variable only when the deletion from Firebase is successful.

_deleteTodo(String todoId, int index) {
  _database.reference().child("todo").child(todoId).remove().then((_) {
    print("Delete $todoId successful");
    setState(() {
      _todoList.removeAt(index);
    });
  });
}

Demo

Here is how the application looks like

Demo of final app

Github

Source code available:

https://github.com/tattwei46/flutter_login_demo

Appreciation

Thank you for taking time to read this post. I hope it helps you on your wonderful journey with Flutter. If you find it helpful, please 👏👏👏 to encourage me to write more articles like this 😎

About the author

Founder of tattweicheah.com. Loves music, sport and most importantly software development.

Leave a Reply

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