Flutter + Firebase : Basic(CRUD) App Tutorial

Build your first CRUD-featured app (NoteTaking).

Ashutosh Yadav
7 min readFeb 2, 2024

First, create a flutter project using the flutter create <name of project> command and then type code <project_name> to open in vsCode or create using Android Studio.

creating a new flutter project

Your project will open in the vsCode with all boilerplate code written. Before changing anything let's add our app with the firebase. Enter the following code in the terminal.

firebase login
dart pub global activate flutterfire_cli

Now that your Flutterfire cli is now activated go ahead and create a new project in your Firebase console.

After you have created your Firebase project, head back to vsCode and write the following command in the terminal.

flutterfire configure

All your Firebase projects will be shown in the terminal, select the name of your project and continue, and then it will ask you for which device you want your app.

select your devices as you like.

And that's it, your app is now Firebase-ready. Let's move forward to the coding part now.

Come to main.dart, we have to make some changes here, so that our app gets initialized with Firebase. This is like telling our main() function that it is linked with Firebase now using await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform),

import 'package:basic_crud_app/firebase_options.dart';
import 'package:basic_crud_app/homepage.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({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}

Your pubspec.yaml file dependencies should look like this ( you can ignore google_fonts package).

Now let's create a homepage screen. which will just return the scaffold only for now.

Now we will move on to create a FireBase services class in new file which will contain all the logic of creating/reading/updating/and deleting the notes. So just create a firebase_services.dart file in the lib folder. Your lib folder should look something like this.

Now go on to firebase_services.dart and create a class named FirestoreServices or anything similar and this class will contain methods for creating /reading/updating and deleting. I have tried my best to explain what's going. Do read the //comments I have provided in the code, this will make you understand the code more easily.

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreServices {
// creating a collection named 'notes' under which
// all the new notes will be stored.
final CollectionReference? notes =
FirebaseFirestore.instance.collection('notes');

// adding a new note logic within our 'notes' collection
// by creating 2 fields named
// 'note' (note we enter) and 'timestamp'(time of entry)

Future<void> addNote(String note) {
return notes!.add(
{'note': note, 'timestamp': Timestamp.now()},
);
}

// reading data within the 'notes' collection
// we have made earlier in the form of snapshots

Stream<QuerySnapshot> showNotes() {
final notesStream =
notes!.orderBy('timestamp', descending: true).snapshots();

return notesStream;
}

// update the data by accessing the particular
// docId of the note which we want to update.

Future<void> updateNotes(String docId, String newNote, Timestamp time) {
return notes!.doc(docId).update({'note': newNote, 'timestamp': time});
}

// delete the data by accessing the particular
// which we want to delete.

Future<void> deleteNote(String docId) {
return notes!.doc(docId).delete();
}
}

Let's understand the code inside the FirestoreServices class, first of all, we have created a collection named ‘notes’ which of type CollectionReference provided by the core_firestore package. This is like an entry point inside your firebase or to be more accurate firestore database.

Let's look at all the methods one by one inside our class FirestoreServices.

  1. addNote() (for adding new note)
// adding a new note logic within our 'notes' collection 
// by creating 2 fields named
// 'note' (note we enter) and 'timestamp'(time of entry)

Future<void> addNote(String note) {
return notes!.add(
{'note': note, 'timestamp': Timestamp.now()},
);
}

If you look at the addNote() method, what you will find is nothing but adding a new ‘note’ and ‘timestamp’ field in your ‘notes’ collection. Let me show you how it looks inside the Firestore database this will clear it up much more.

Firestore Database (collection->documents ->fields)

As you can see inside the ‘notes’ collection we have our documents (having unique docId ) and when we click on any documents we can see different fields we have assigned. I think this will help you to understand what’s happening inside Firestore.

Let's move forward and look at the showNotes() function which will allow our app to show different notes on the screen.

2. showNotes() (for showing notes on the app screen)

// reading data within the 'notes' collection
// we have made earlier in the form of snapshots

Stream<QuerySnapshot> showNotes() {
final notesStream =
notes!.orderBy('timestamp', descending: true).snapshots();

return notesStream;
}

As you can see showNotes() will return Stream<QuerySnapshots> which means it will return stream (continuous flow) of snapshot (document) from your database, and we have ordered it in such way that the latest document will shown first, so that you can see your newly added note on the top of the screen.

3. updateNotes() (for updating your changes/editing notes)

// update the data by accessing the particular
// docId of the note which we want to update.

Future<void> updateNotes(String docId, String newNote, Timestamp time) {
return notes!.doc(docId).update({'note': newNote, 'timestamp': time});
}

As you can see this method requires 3 arguments namely doctId, newNote, and time, which we will pass when we call this method at the homepage() inside the edit button widget’s onPressed(){} function (you will find the code ahead). Now why we need these arguments ? Simply because we should have access to note which we are going to edit, and that can be done by passing docId. As you can see in the image provided, we can only access the particular note fields by clicking on the docId. I hope it's clear. The newNote is passed so that we can use newNote as the updated note for the ‘notes’ field, similarly we pass time to update the ‘timestamp’ with the initial time because we do not want to edit time also when we update our note or you can just ignore the time argument and create a field as ‘timestamp’ : Timestamp.now(). I know it’s a bit tricky to understand but everything will get clear when you look at the homePage() file code.

4. deleteNote() (for deleting the note)

Finally for deleting, we just access the docId of the note you want to delete and simply use .delete() method to remove it from your database.

That's all you have to know to build a basic CRUD app. I will provide you the homepage() code.

import 'dart:ui';

import 'package:basic_crud_app/firebase_services.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:google_fonts/google_fonts.dart';

class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
TextEditingController controller = TextEditingController();
FirestoreServices firestoreServices = FirestoreServices();
void showNoteBox(String? textToedit, String? docId, Timestamp? time) {
showDialog(
context: context,
builder: (context) {
if (textToedit != null) {
controller.text = textToedit;
}
return AlertDialog(
title: Text(
"Add note",
style: GoogleFonts.alexandria(fontSize: 16),
),
content: TextField(
decoration: InputDecoration(hintText: 'Note here...'),
style: GoogleFonts.alexandria(),
controller: controller,
),
actions: [
ElevatedButton(
onPressed: () {
if (docId == null) {
firestoreServices.addNote(controller.text);
} else {
firestoreServices.updateNotes(docId, controller.text, time!);
}
controller.clear();
Navigator.pop(context);
},
child: Text(
'add',
style: GoogleFonts.alexandria(),
),
)
],
);
},
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.purple[50],
appBar: AppBar(
centerTitle: true,
backgroundColor: Colors.purple[50],
title: Text(
"C R U D",
style: GoogleFonts.alexandria(),
),
),
floatingActionButton: FloatingActionButton.extended(
backgroundColor: Colors.purple[100],
label: Text(
'add',
style: GoogleFonts.alexandria(fontSize: 18),
),
icon: Icon(Icons.add),
onPressed: () async {
showNoteBox(null, null, null);
},
),
body: StreamBuilder(
stream: FirestoreServices().showNotes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List noteList = snapshot.data!.docs;
return ListView.builder(
itemCount: noteList.length,
itemBuilder: (context, index) {
DocumentSnapshot document = noteList[index];
String docId = document.id;
Map<String, dynamic> data =
document.data() as Map<String, dynamic>;
String note = data['note'];
Timestamp time = data['timestamp'];
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: ListTile(
contentPadding: EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
tileColor: Colors.purple[100],
title: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
note,
style: GoogleFonts.alexandria(
textStyle: TextStyle(
color: Colors.purple[800], fontSize: 19)),
),
),
trailing: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
color: Colors.purple[400],
icon: Icon(Icons.edit),
onPressed: () {
showNoteBox(note, docId, time);
},
),
IconButton(
color: Colors.purple[400],
onPressed: () {
firestoreServices.deleteNote(docId);
},
icon: Icon(Icons.delete))
],
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
time.toDate().hour.toString(),
style: const TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold),
),
Text(":"),
Text(
time.toDate().minute.toString(),
style: const TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold),
),
],
),
)
],
);
},
);
} else {
return Center(
child: Text("Nothing to show...add notes"),
);
}
},
),
);
}
}

I know the code is horrible at first glance but worry not, its pretty simple. The only thing you need to know in this code is the StreamBuilder() which is the body of our Scaffold. So StreamBuilder() requires a stream source (which in our case is from showNotes() method from our FirestoreService class) and a builder that will return build our screen to reflect our note and in our app the builder is ListviewBuilder.

This is what your app would look like.

Basic Notetaking app

source code: https://github.com/Kryptonite-18/crud_app.git

--

--