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
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 Facebook, Linkedin, Github, YouTube,
BuyMeACoffee, and Instagram.
Contribute: BuyMeACoffee
Contact: Contact Us
-
Hey people!!!!!
Good mood and good luck to everyone!!!!! -
Hey people!!!!!
Good mood and good luck to everyone!!!!!
Leave a Reply