Creating a ToDo app with Firebase is an essential project for newcomers to grasp a new tech stack. Much like the traditional “Hello World,” it provides invaluable learning opportunities. There is a lot to learn by implementing these mini-projects.

This course is a complete tutorial from Login to Performing actions along with the logout.

Before diving into the project, let’s discuss what we’ll be discussing in this project. You shall be able to:

  • Register with email and password.
  • Google login
  • Add a new task using the floating action button(Create)
  • View all the added tasks. (Read)
  • Edit the task. (Update)
  • Remove task from list. (Delete)

Simply by enumerating the CRUD operations mentioned earlier, it becomes evident that a database will be essential for data storage. This could take the form of a relational database like SQLite, or a non-relational one like Firebase or MongoDB. In our project, we’ll opt for Firebase.

By the conclusion of this tutorial, we aim to have developed a functional to-do list application, similar to the one depicted below.

Final Demo

Todo app with firbebase firestore

This project is divided into three parts.

  • Create and set up Firebase to project
  • To perform secure authentication with Firebase in your Flutter application
  • Then perform and achieve our goal which this article is about i.e create a todo application

Let’s dive

Step A: Create and set up Firebase to project

First, you need to set up a firebase in your application. To set up follow the link below:

Step B Perform secure authentication with Firebase in your Flutter application

You need to add authentication to store the information in the specific user database. With this, your task would be only visible to you and the same for other users. To perform authentication follow this link:

Step C: ToDo App With Firebase (CRUD)

First, we need to add the dependencies in the pubsec.yaml file

Add dependencies

dependencies:

////other dependencies
  firebase_core: ^2.24.2
  cloud_firestore: ^4.9.1

  intl: ^0.19.0

Here firebase_core and cloud_firestore are for the server interaction with Firebase. And intl is for a date.

Create Model

After adding the dependencies, you now need to create an object model of the data to be saved in our Firestore database.

Create a file named todo.dart in the lib/model directory.

class Todo {
  String? id;
  String title;
  String description;
  String date;
  bool completed;

  Todo({
    required this.id,
    required this.title,
    required this.description,
    required this.date,
    required this.completed,
  });

  Todo copyWith({
    String? id,
    String? title,
    String?description,
    String? date,
    bool? completed,
  }) {
    return Todo(
      id: id ?? this.id,
      title: title ?? this.title,
      completed: completed ?? this.completed,
      description: description ?? this.description,
      date: date ?? this.date,
    );
  }
}

Setup Repo

Create a file named firestore_repo.dart inside lib/repository. Here we connect our Firestore database with the collection.

 final CollectionReference _todosCollection =
      FirebaseFirestore.instance.collection('$YOUR_REPO_NAME'); 

Get the currently logged-in user.

 String get currentUserId =>
      FirebaseAuth.instance.currentUser?.uid ?? 'default';

To perform an add operation

 Future<void> addTodo(Todo todo) {
    return _todosCollection
        .doc(currentUserId)
        .collection('user_todos')
        .add({
      'title': todo.title,
      'description':todo.description,
      'date':todo.date,
      'completed': todo.completed,
    
    });
  }

Update Operation

Future<void> updateTodo(String todoId, Todo todo) {
    return _todosCollection
        .doc(currentUserId)
        .collection('user_todos')
        .doc(todoId)
        .update({
      'title': todo.title,
        'description':todo.description,
      'date':todo.date,
      'completed': todo.completed,
    });
  }

Delete

 Future<void> deleteTodo(String todoId) {
    return _todosCollection
        .doc(currentUserId)
        .collection('user_todos')
        .doc(todoId)
        .delete();
  }

Setup BloC

A bloc contains events, states, and blocs. Firstly, let’s create states and events for the Bloc. After creating states and events, we’ll create a TodoBloc which will operate the logic by using the events, states, and services we have created.

TodoState

Create a file named todo_state.dart inside lib/bloc/todo .Inside we are going to define all the states our todo process will go through.

@immutable
abstract class TodoState {}

class TodoInitial extends TodoState {}

class TodoLoading extends TodoState {}

class TodoLoaded extends TodoState {
  final List<Todo> todos;

  TodoLoaded(this.todos);
}

class TodoOperationSuccess extends TodoState {
  final String message;

  TodoOperationSuccess(this.message);
}

class TodoError extends TodoState {
  final String errorMessage;

  TodoError(this.errorMessage);
}
TodoEvent

Create a file named todo_event.dart inside lib/bloc/todo .Inside we are going to define all the events our todo process will go through.

@immutable
abstract class TodoEvent {}

class LoadTodos extends TodoEvent {}

class AddTodo extends TodoEvent {
  final Todo todo;

  AddTodo(this.todo);
}


class UpdateTodo extends TodoEvent {
  final String todoId;
  final Todo todo;
  UpdateTodo(this.todoId,this.todo);
}
class DeleteTodo extends TodoEvent {
  final String todoId;

  DeleteTodo(this.todoId);
}
TodoBloc

This TodoBloc will handle the overall activity, from what happens when the user clicks a button to what is displayed on the screen. It also communicates with the Firebase service we created.

Create todo_bloc.dart file inside your project’s bloc/todo directory.

Add the following code to define the TodoBloc class.

class TodoBloc extends Bloc<TodoEvent, TodoState> {
  final FirestoreService _firestoreService;

  TodoBloc(this._firestoreService) : super(TodoInitial()) {
    on<LoadTodos>((event, emit) async {
      try {
        emit(TodoLoading());
        final todos = await _firestoreService.getTodos().first;
        emit(TodoLoaded(todos));
      } catch (e) {
        emit(TodoError('Failed to load todos.'));
      }
    });

    on<AddTodo>((event, emit) async {
      try {
        emit(TodoLoading());
        await _firestoreService.addTodo(event.todo);
        emit(TodoOperationSuccess('Todo added successfully.'));
      } catch (e) {
        emit(TodoError('Failed to add todo.'));
      }
    });

    on<UpdateTodo>((event, emit)  async {
      try {
        emit(TodoLoading());
        await _firestoreService.updateTodo(event.todoId,event.todo);
        emit(TodoOperationSuccess('Todo updated successfully.'));
      } catch (e) {
        emit(TodoError('Failed to update todo.'));
      }
    });

    on<DeleteTodo>((event, emit) async {
      try {
        emit(TodoLoading());
        await _firestoreService.deleteTodo(event.todoId);
        emit(TodoOperationSuccess('Todo deleted successfully.'));
      } catch (e) {
        emit(TodoError('Failed to delete todo.'));
      }
    });

  }
}

Perform operations

Now from the homepage.dart we can perform the CRUD operations when user clicks or performs actions.

First, we create the method to show the dialog box.

void _showAddTodoDialog(BuildContext context, bool isEdit, Todo? todos) {
    final titleController = TextEditingController();
    final descriptionController = TextEditingController();
    final dateController = TextEditingController();
    if (isEdit) {
      titleController.text = todos!.title;
      descriptionController.text = todos.description;
      dateController.text = todos.date;
    }
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text(isEdit ? 'Edit Todo' : 'Add Todo'),
          content: SizedBox(
            width: MediaQuery.of(context).size.width * 0.5,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                TextField(
                  controller: titleController,
                  style: const TextStyle(fontSize: 14),
                  decoration: InputDecoration(
                    contentPadding: const EdgeInsets.symmetric(
                      horizontal: 20,
                      vertical: 20,
                    ),
                    hintText: 'Task',
                    hintStyle: const TextStyle(fontSize: 14),
                    icon: const Icon(CupertinoIcons.square_list,
                        color: AppColors.appColor),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(15),
                    ),
                  ),
                ),
                const SizedBox(height: 15),
                TextFormField(
                  controller: descriptionController,
                  keyboardType: TextInputType.multiline,
                  maxLines: null,
                  style: const TextStyle(fontSize: 14),
                  decoration: InputDecoration(
                    contentPadding: const EdgeInsets.symmetric(
                      horizontal: 20,
                      vertical: 20,
                    ),
                    hintText: 'Description',
                    hintStyle: const TextStyle(fontSize: 14),
                    icon: const Icon(CupertinoIcons.bubble_left_bubble_right,
                        color: AppColors.appColor),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(15),
                    ),
                  ),
                ),
                const SizedBox(height: 15),
                TextField(
                  controller:
                      dateController, //editing controller of this TextField
                  style: const TextStyle(fontSize: 14),
                  decoration: InputDecoration(
                    contentPadding: const EdgeInsets.symmetric(
                      horizontal: 20,
                      vertical: 20,
                    ),
                    hintText: 'Date',
                    hintStyle: const TextStyle(fontSize: 14),
                    icon: const Icon(CupertinoIcons.calendar,
                        color: AppColors.appColor),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(15),
                    ),
                  ),
                  readOnly: true, // when true user cannot edit text
                  onTap: () async {
                    DateTime? pickedDate = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now(), //get today's date
                        firstDate: DateTime(
                            2000), //DateTime.now() - not to allow to choose before today.
                        lastDate: DateTime(2101));

                    if (pickedDate != null) {
                      //get the picked date in the format => 2022-07-04 00:00:00.000
                      String formattedDate = DateFormat('yyyy-MM-dd').format(
                          pickedDate); // format date in required form here we use yyyy-MM-dd that means time is removed
                      print(
                          formattedDate); //formatted date output using intl package =>  2022-07-04
                      //You can format date as per your need

                      setState(() {
                        dateController.text =
                            formattedDate; //set foratted date to TextField value.
                      });
                    } else {
                      print("Date is not selected");
                    }
                  },
                ),
              ],
            ),
          ),
          actions: [
            ElevatedButton(
              child: const Text('Cancel'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
            ElevatedButton(
              child: Text(isEdit ? 'Update' : 'Add'),
              onPressed: () {
                final todo = isEdit
                    ? Todo(
                        id: todos!.id!,
                        title: titleController.text,
                        description: descriptionController.text,
                        date: dateController.text,
                        completed: titleController.text.isEmpty)
                    : Todo(
                        id: DateTime.now().toString(),
                        title: titleController.text,
                        description: descriptionController.text,
                        date: dateController.text,
                        completed: false,
                      );
                if (isEdit) {
                  var updatedTo =
                      todo.copyWith(completed: titleController.text.isNotEmpty);
                  BlocProvider.of<TodoBloc>(context)
                      .add(UpdateTodo(todo.id!, updatedTo));
                  Navigator.pop(context);
                } else {
                  BlocProvider.of<TodoBloc>(context).add(AddTodo(todo));
                  Navigator.pop(context);
                }
              },
            ),
          ],
        );
      },
    );
  }

When the user clicks the FloatingActionButton we create a dialog with the fields.

 FloatingActionButton(
        backgroundColor: AppColors.appColor,
        onPressed: () {
          _showAddTodoDialog(context, false, null);
        },
        child: const Icon(Icons.add,color: Colors.white,),
      ),

When the user clicks the edit icon.

  IconButton(
                              icon: const Icon(Icons.edit),
                              onPressed: () {
                                _showAddTodoDialog(context, true, todo);
                              },
                            ),

When the user clicks the delete icon.

IconButton(
                              icon: Icon(
                                Icons.delete,
                                color: Colors.red.withOpacity(0.5),
                              ),
                              onPressed: () {
                                _todoBloc.add(DeleteTodo(todo.id!));
                              },
                            ),

Run

When completed you can now run your Flutter app and test the registration, login, and logout functionalities. If you want to clone the repo, you can check it out on GitHub here and leave a like.

Conclusion

In this article, we explored building an application that allows us to perform our tasks. Here we created a ToDo App With Firebase in Flutter.

We learned how to set up Firebase in a Flutter project, create Blocs for forming CRUD operations, and implement the process flow using Bloc.

By leveraging the power of Firebase and the predictability of Bloc, you can ensure a secure and seamless user experience in your Flutter apps.

Thanks for reading this article.  ❤

Also, follow to get updated on exciting articles and projects.

If I got something wrong? Let me know in the comments. I would love to improve.

Let’s get connected

We can be friends. Find on FacebookLinkedinGithubYouTube

BuyMeACoffee, and Instagram.

Contribute: BuyMeACoffee

ContactContact Us

  1. X22jen Avatar
    X22jen

    Hey people!!!!!
    Good mood and good luck to everyone!!!!!

  2. XRumer23jen Avatar
    XRumer23jen

    Hey people!!!!!
    Good mood and good luck to everyone!!!!!

Leave a Reply

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

ToDo App With Firebase; its file name is Firebase-1024x576.png