I Made a Simple App Using Flutter and Firebase Storage DB.

Gerwin Jo
6 min readApr 9, 2023

--

Today, I will demonstrate you how to integrate our Flutter app with Firebase DB. In this case, I will show you with simple todos app how it works.

Part 1: Firebase Storage and Setup

Before you can integrate your app to Firebase, you have to make an environment for Firebase. It include how to add projects, and integrate it with your app in Flutter.

Go to their website https://console.firebase.google.com/u/0/?hl=id, for further information and add projects. Just follow step by step from Create Project until Enable Google Analytics (optional).

Homepage Firebase

Part 2: Firebase Storage (Firestore)

There are 2 databases that support Firebase. There are Firestore DB (it called Cloud Storage) and Realtime Database. Today, we will give a look for Firestore DB.

How we configure it into our app?

Go to Firebase Database, and we do some settings.

Homepage console for Cloud Firestore

As you can see, this is Firebase Cloud Firestore, it is similiar to cloud database, you can do CRUD (Create-Read-Update-Delete) and do some queries. There are 4 tabs:

  • Data: It include your datas. There are collections and documents. Collections are list of objects. It is similiar like initializing table in SQL. Documents are list of datas. It have IDs and fields. 1 document include random IDs and fields. So, you don’t need to have id in your field.
  • Rules: Depends how you treat your database. It is written in form like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}

At first, you will see allow read, write : if false. Therefore, you can’t read/write the db. We set true, so you can write and read (do CRUD) with your app.

  • Indexes: Depends how you indexing db. It can be single field or multiple field, based on your need. Firestore have automatic indexing by default.
  • Usage: You will see your billings if you have billing plan. In this tab, you will see CRUD on your database affects costs.

Part 3: Configure in Flutter App

After sets up your DB, now we turn into apps.

First, things first.

You have to add your pubspec.yaml packages like this:

cloud_firestore: ^4.5.0
firebase_storage: ^11.1.0
firebase_core: ^2.9.0

You have to configure your main_app.dart with Firebase Configuration like this:

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: const FirebaseOptions(
apiKey: "",
appId: "",
messagingSenderId: "",
projectId: "",
authDomain: "",
databaseURL: "",
storageBucket: "",
measurementId: "",
),
);
runApp(const MyApp());
}

Where I can get this configuration? Since I use Flutter for web, then you have to add Web App. Add the config and check Project Settings > General > Your App.

Tada. If you have your config correctly, that we will begin into implementation.

Part 4: Implementation

We are going to make our todo app.

The most important about this is CRUD Firebase itself. So, we will make a controller, it include save, delete, update, and get. The code will be like this:

import 'package:automation/models/todo_models.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:get/get.dart';

class TodoController extends GetxController {
FirebaseFirestore? DB;

@override
void onInit() {
DB = FirebaseFirestore.instance;
super.onInit();
}

getTodo(String docId) {
DB?.collection("todo").doc(docId).get();
}

saveTodo(TodoModel todoModel) {
DB?.collection("todo").add(todoModel.toJson());
}

updateTodo(String docId, TodoModel todoModel) {
DB?.collection("todo").doc(docId).update(todoModel.toJson());
}

deleteTodo(String docId) async {
DB?.collection("todo").doc(docId).delete();
}
}

Why we don’t need to define id in model? Firebase Firestore have automatic random IDs features. We will use the automatic IDs to update and delete datas.

The model will be like this:

import 'package:json_annotation/json_annotation.dart';

part 'todo_models.g.dart';

@JsonSerializable()
class TodoModel {
int? id;
String? title;
String? description;
dynamic dateStart;
dynamic dateEnd;
dynamic dateCreated;

TodoModel({
this.id,
this.title,
this.description,
this.dateStart,
this.dateEnd,
this.dateCreated,
});

factory TodoModel.fromJson(Map<String, dynamic> json) =>
_$TodoModelFromJson(json);
Map<String, dynamic> toJson() => _$TodoModelToJson(this);
}

We created TodoModel for interaction between UI and Firestore.

We will have 2 screens, the first one is home_screen.dart and second is todo_screen.dart. It will show like this:

import 'package:automation/controllers/home_controller.dart';
import 'package:automation/controllers/todo_controller.dart';
import 'package:automation/models/todo_models.dart';
import 'package:automation/screens/todo_screens.dart';
import 'package:automation/utils/date_utils.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';

class HomeScreens extends StatelessWidget {
const HomeScreens({super.key});

@override
Widget build(BuildContext context) {
HomeController homeController = Get.put(HomeController());
TodoController todoController = Get.put(TodoController());

return SafeArea(
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: false,
title: const Text("Todo List App"),
),
body: FutureBuilder(
future: homeController.getTodo(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data?.docs.length,
itemBuilder: (BuildContext context, int index) {
DocumentSnapshot data = snapshot.data!.docs[index];
var getData = data.data() as Map<String, dynamic>;
TodoModel model = TodoModel.fromJson(getData);

model.dateStart =
convertTimestampToDate(getData["dateStart"]);
model.dateEnd = convertTimestampToDate(getData["dateEnd"]);
model.dateCreated =
convertTimestampToDate(getData["dateCreated"]);
return ListTile(
onTap: () {
Get.to(
TodoScreens(
id: data.id,
todoModel: model,
),
)?.then(
(value) {
homeController.getTodo();
},
);
},
title: Text(model.title!),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(model.description!),
Text(
"${DateFormat.yMEd().format(model.dateStart)} - ${DateFormat.yMEd().format(model.dateEnd)}",
),
],
),
trailing: IconButton(
onPressed: () async {
Get.dialog(
Dialog(
elevation: 1,
child: SizedBox(
height: 150,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Are you want to delete this item?",
),
const Divider(
color: Colors.transparent,
),
Row(
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Get.offAll(const HomeScreens());
},
child: const Text("No"),
),
const SizedBox.square(
dimension: 10,
),
ElevatedButton(
onPressed: () async {
await todoController
.deleteTodo(data.id);
Get.offAll(const HomeScreens());
Get.snackbar(
"Success",
"Data was successfully deleted!",
icon: const Icon(
Icons.check_circle,
),
mainButton: TextButton(
onPressed: () async {
await todoController
.saveTodo(model);
Get.offAll(
const HomeScreens());
},
child: const Text("Undo"),
),
dismissDirection:
DismissDirection.horizontal,
shouldIconPulse: true,
snackPosition:
SnackPosition.BOTTOM,
animationDuration:
const Duration(seconds: 1),
);
},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(
Colors.redAccent,
),
),
child: const Text("Yes"),
),
],
)
],
),
),
),
);
/* showDialog(
context: context,
builder: (context) => ,
); */
},
icon: const Icon(Icons.delete_rounded),
),
);
});
} else if (snapshot.connectionState == ConnectionState.none) {
return const Center(
child: Text("No data"),
);
}
return const Center(child: CircularProgressIndicator());
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Get.to(TodoScreens())?.then((value) => homeController.getTodo());
},
label: const Text(
"Add Notes",
),
icon: const Icon(
Icons.add,
),
),
),
);
}

dynamic convertTimestampToDate(dynamic value) {
Timestamp timestamp = value as Timestamp;
return DateTime.fromMillisecondsSinceEpoch(
timestamp.millisecondsSinceEpoch);
}
}
import 'package:automation/common/reusable_screens.dart';
import 'package:automation/common/styles.dart';
import 'package:automation/models/todo_models.dart';
import 'package:automation/screens/home_screens.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:automation/controllers/todo_controller.dart';
import 'package:intl/intl.dart';

class TodoScreens extends StatelessWidget {
TodoScreens({this.id, this.todoModel, super.key});
final GlobalKey<FormState> form = GlobalKey<FormState>();
final String? id;
final TodoModel? todoModel;

@override
Widget build(BuildContext context) {
TextEditingController titleController = TextEditingController();
TextEditingController titleDescriptionController = TextEditingController();
TextEditingController dateTimeController = TextEditingController();
TodoController todoController = Get.put(TodoController());

if (id != null) {
titleController.text = todoModel!.title!;
titleDescriptionController.text = todoModel!.description!;
dateTimeController.text =
"${DateFormat.yMEd().format(todoModel!.dateStart)} - ${DateFormat.yMEd().format(todoModel!.dateEnd)}";
ReusableScreens.dateTimeStart = todoModel!.dateStart;
ReusableScreens.dateTimeEnd = todoModel!.dateEnd;
ReusableScreens.dateTimeRange = DateTimeRange(
start: ReusableScreens.dateTimeStart!,
end: ReusableScreens.dateTimeEnd!,
);
}

return SafeArea(
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text("Create Todo App"),
),
body: Form(
key: form,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ListView(
padding: Styles().paddingForm,
physics: const ScrollPhysics(),
children: [
Styles().space,
ReusableScreens.textFormFieldReusable(titleController, "Title",
mandatory: true),
Styles().space,
Styles().space,
ReusableScreens.textFormFieldReusable(
titleDescriptionController, "Description",
mandatory: false),
Styles().space,
Styles().space,
ReusableScreens.dateTimePickerReusable(
context, dateTimeController, "Time",
mandatory: false),
Styles().space,
],
),
),
bottomNavigationBar: ReusableScreens.buttonElevatedReusable(() {
if (id != null) {
todoController.updateTodo(
id!,
TodoModel(
title: titleController.text,
description: titleDescriptionController.text,
dateStart: Timestamp.fromDate(ReusableScreens.dateTimeStart!),
dateEnd: Timestamp.fromDate(ReusableScreens.dateTimeEnd!),
dateCreated: Timestamp.fromDate(DateTime.now()),
),
);
} else {
todoController.saveTodo(
TodoModel(
title: titleController.text,
description: titleDescriptionController.text,
dateStart: Timestamp.fromDate(ReusableScreens.dateTimeStart!),
dateEnd: Timestamp.fromDate(ReusableScreens.dateTimeEnd!),
dateCreated: Timestamp.fromDate(DateTime.now()),
),
);
}
Get.off(
const HomeScreens(),
);
})),
);
}
}

If you implement it correctly, the app will be like this:

Todo App

Here are my experiment for this app:

--

--

Gerwin Jo

Application Development Specialist @ PT. Surya Madistrindo | Enthusiast @Art 🎨, Music 🎵, and Technology 💻.