Building mobile apps with dynamic data has never been easier, thanks to the powerful combination of Flutter and Firebase Cloud Firestore. In this tutorial, you’ll learn how to build a fully functional CRUD (Create, Read, Update, Delete) mobile app using Flutter that connects to Firebase's NoSQL Cloud Firestore in real time.
Whether you're a Flutter beginner or looking to sharpen your Firebase integration skills, this hands-on guide will walk you through creating a beautifully styled, multi-page Flutter app. You’ll learn to manage Firestore data effectively, navigate between screens, and give your UI a polished look using Flutter’s built-in design capabilities.
By the end of this tutorial, you’ll have a solid foundation for creating your data-driven apps, from to-do lists and notes apps to more complex solutions, all powered by Firebase’s scalable cloud backend.
Prerequisites
Before we begin, make sure you have the following tools and setup ready:
- Flutter SDK installed and configured
- An IDE such as VS Code or Android Studio
- A Firebase account with a project created
- Firebase Firestore is enabled in the Firebase console
- An Android Emulator, an iOS Simulator, or a real device for testing
You should also be comfortable with basic Flutter concepts, such as widgets, navigation, and state management.
Step 1: Create a New Flutter Project
First, let's create a new Flutter project using the command line:
flutter create flutter_firebase_crud
cd flutter_firebase_crud
Open the project in your preferred code editor and remove the boilerplate code inside lib/main.dart.
Step 2: Add Dependencies
Next, open the pubspec.yaml file and add the required Firebase dependencies:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
firebase_core: ^2.27.0
cloud_firestore: ^4.15.5
Then, run the following command to install the new dependencies:
flutter pub get
Also, ensure that you configure platform-specific Firebase settings for both Android and iOS.
Step 3: Set Up Firebase with Flutter
Android Setup
1. Go to the Firebase Console and add an Android app to your project.
2. Register the package name (e.g., com.djamware.flutter_firebase_crud). Make sure your android/app/build.gradle.kts have the same namespace.
android {
namespace = "com.djamware.flutter_firebase_crud"
...
defaultConfig {
applicationId = "com.djamware.flutter_firebase_crud"
...
}
}
3. Rename the folder "android/app/src/main/kotlin/com/example" to "android/app/src/main/kotlin/com/djamware". Also, update MainActivity.kt.
package com.djamware.flutter_firebase_crud
4. Update in AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.djamware.flutter_firebase_crud">
5. Download the google-services.json file and place it in android/app/.
6. Update your android/build.gradle.kts and android/app/build.gradle.kts as instructed by Firebase.
iOS Setup
- Register the iOS bundle ID in Firebase.
- Download GoogleService-Info.plist and place it in ios/Runner/.
- Open ios/Runner.xcworkspace in Xcode and ensure the file is added to the project.
- Add FirebaseApp.configure() in AppDelegate.swift.
Initialize Firebase in main.dart
Replace the contents of lib/main.dart with the following to initialize Firebase before the app starts:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase CRUD App',
theme: ThemeData(primarySwatch: Colors.indigo),
home: Scaffold(
appBar: AppBar(title: Text('Firebase CRUD Home')),
body: Center(child: Text('Welcome to Firebase CRUD App')),
),
);
}
}
At this point, your app is connected to Firebase and ready to interact with Cloud Firestore!
Step 4: Project Structure
To keep the app organized and maintainable, we’ll use a modular folder structure. Inside the lib/ directory, create the following folders and files:
lib/
├── main.dart
├── models/
│ └── note_model.dart
├── services/
│ └── firestore_service.dart
├── screens/
│ ├── home_screen.dart
│ └── add_edit_screen.dart
Explanation:
- models/: Contains data models (e.g., Note).
- services/: Contains Firestore operations such as add, read, update, and delete.
- screens/: Contains all the UI pages (Home and Add/Edit).
- main.dart: Entry point of the app with routing configuration.
This structure helps keep your code clean and scalable as the app grows.
Step 5: Create Firestore Service
Now let’s build a service to handle all communication with Cloud Firestore. This service will allow us to abstract CRUD operations away from UI code.
Create a new file at lib/services/firestore_service.dart:
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/note_model.dart';
class FirestoreService {
final CollectionReference notesCollection =
FirebaseFirestore.instance.collection('notes');
// Create or update note
Future<void> setNote(Note note) {
return notesCollection.doc(note.id).set(note.toMap());
}
// Get notes as a stream
Stream<List<Note>> getNotes() {
return notesCollection.snapshots().map((snapshot) {
return snapshot.docs.map((doc) {
return Note.fromMap(doc.id, doc.data() as Map<String, dynamic>);
}).toList();
});
}
// Delete a note
Future<void> deleteNote(String id) {
return notesCollection.doc(id).delete();
}
}
This FirestoreService class provides:
- setNote(): Adds or updates a note in Firestore.
- getNotes(): Streams a real-time list of notes.
- deleteNote(): Deletes a note by ID.
Next, we’ll define the Note model and build the UI screens to display and manage data.
Step 6: Create the Note Model
To represent our data in a structured way, we’ll define a Note model. This model will help map Firestore documents to Dart objects and vice versa.
Create a file at lib/models/note_model.dart:
class Note {
final String id;
final String title;
final String content;
Note({
required this.id,
required this.title,
required this.content,
});
// Convert Note to Map for Firestore
Map<String, dynamic> toMap() {
return {
'title': title,
'content': content,
};
}
// Create Note from Firestore Map
factory Note.fromMap(String id, Map<String, dynamic> map) {
return Note(
id: id,
title: map['title'] ?? '',
content: map['content'] ?? '',
);
}
}
This simple data model includes an ID, title, and content, and provides helper methods to convert between Firestore maps and Dart objects.
Step 7: Build the Home Screen
The Home Screen will display a list of notes retrieved from Firestore in real time. We’ll use a StreamBuilder to listen for updates and show them using a ListView.
Create a file at lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import '../services/firestore_service.dart';
import '../models/note_model.dart';
class HomeScreen extends StatelessWidget {
final FirestoreService firestoreService = FirestoreService();
HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('My Notes')),
body: StreamBuilder<List<Note>>(
stream: firestoreService.getNotes(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No notes found.'));
}
final notes = snapshot.data!;
return ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
final note = notes[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
title: Text(note.title),
subtitle: Text(note.content),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => firestoreService.deleteNote(note.id),
),
onTap: () {
Navigator.pushNamed(context, '/add-edit', arguments: note);
},
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.pushNamed(context, '/add-edit'),
child: Icon(Icons.add),
),
);
}
}
Features:
- Real-time Firestore updates with StreamBuilder
- Displays notes in a scrollable list
- Tap a note to edit it
- Delete a note using the trash icon
- Add new notes using the FAB
Step 8: Add/Edit Note Screen
The Add/Edit screen allows users to create new notes or edit existing ones. It uses a simple form with two TextFormFields and a Save button.
Add this dependency to the pubspec.yaml:
dependencies:
...
uuid: ^4.5.1
Create the file lib/screens/add_edit_screen.dart:
import 'package:flutter/material.dart';
import '../models/note_model.dart';
import '../services/firestore_service.dart';
import 'package:uuid/uuid.dart';
class AddEditScreen extends StatefulWidget {
final Note? note;
const AddEditScreen({super.key, this.note});
@override
AddEditScreenState createState() => AddEditScreenState();
}
class AddEditScreenState extends State<AddEditScreen> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _contentController = TextEditingController();
final FirestoreService firestoreService = FirestoreService();
@override
void initState() {
super.initState();
if (widget.note != null) {
_titleController.text = widget.note!.title;
_contentController.text = widget.note!.content;
}
}
void _saveNote() {
if (_formKey.currentState!.validate()) {
final id = widget.note?.id ?? const Uuid().v4();
final note = Note(
id: id,
title: _titleController.text,
content: _contentController.text,
);
firestoreService.setNote(note);
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
final isEditing = widget.note != null;
return Scaffold(
appBar: AppBar(title: Text(isEditing ? 'Edit Note' : 'Add Note')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(labelText: 'Title'),
validator: (value) =>
value == null || value.isEmpty ? 'Enter a title' : null,
),
SizedBox(height: 16),
TextFormField(
controller: _contentController,
decoration: InputDecoration(labelText: 'Content'),
maxLines: 5,
validator: (value) =>
value == null || value.isEmpty ? 'Enter content' : null,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _saveNote,
child: Text(isEditing ? 'Update' : 'Save'),
),
],
),
),
),
);
}
}
Key features:
- Uses a GlobalKey<FormState> for validation
- Pre-fills fields when editing an existing note
- Uses uuid to generate unique IDs for new notes
- Saves data to Firestore via FirestoreService
Step 9: Routing and Navigation
We’ll now set up navigation between the HomeScreen and AddEditScreen. First, update your main.dart:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'models/note_model.dart';
import 'screens/home_screen.dart';
import 'screens/add_edit_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase CRUD App',
theme: ThemeData(primarySwatch: Colors.indigo),
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/add-edit': (context) {
final Note? note =
ModalRoute.of(context)?.settings.arguments as Note?;
return AddEditScreen(note: note);
},
},
);
}
}
Highlights:
- Uses named routes for clean navigation
- Passes the Note object to AddEditScreen via route arguments
✅ At this point, you have a complete Flutter CRUD app using Firebase Firestore—with real-time sync, multi-page navigation, and styled UI!
Step 10: Styling All Screens
We’ll apply a simple and modern look using Flutter’s Material components, spacing, consistent font sizes, and subtle elevation. Here’s how we can style each screen:
Home Screen Styling (home_screen.dart)
Update your ListTile and Scaffold to enhance spacing, padding, and color usage.
✅ Updated home_screen.dart (relevant parts only):
import 'package:flutter/material.dart';
import '../services/firestore_service.dart';
import '../models/note_model.dart';
class HomeScreen extends StatelessWidget {
final FirestoreService firestoreService = FirestoreService();
HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Notes', style: TextStyle(fontWeight: FontWeight.bold)),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: StreamBuilder<List<Note>>(
stream: firestoreService.getNotes(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No notes found.'));
}
final notes = snapshot.data!;
return ListView.separated(
itemCount: notes.length,
separatorBuilder: (_, __) => SizedBox(height: 8),
itemBuilder: (context, index) {
final note = notes[index];
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
title: Text(
note.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
note.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.redAccent),
onPressed: () => firestoreService.deleteNote(note.id),
),
onTap: () => Navigator.pushNamed(
context,
'/add-edit',
arguments: note,
),
),
);
},
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.pushNamed(context, '/add-edit'),
backgroundColor: Colors.indigo,
child: Icon(Icons.add),
),
);
}
}
📝 Add/Edit Screen Styling (add_edit_screen.dart)
Let’s make the form more modern with rounded borders, better spacing, and a styled button.
✅ Updated add_edit_screen.dart (form UI parts only):
...
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
),
validator: (value) => value == null || value.isEmpty ? 'Enter a title' : null,
),
SizedBox(height: 16),
TextFormField(
controller: _contentController,
decoration: InputDecoration(
labelText: 'Content',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
),
maxLines: 6,
validator: (value) => value == null || value.isEmpty ? 'Enter content' : null,
),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _saveNote,
icon: Icon(Icons.save),
label: Text(isEditing ? 'Update Note' : 'Save Note'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
padding: EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
),
),
🌐 Global Styling in main.dart
To apply a consistent font and theme across the app, update main.dart with a custom theme:
theme: ThemeData(
fontFamily: 'Roboto',
primarySwatch: Colors.indigo,
scaffoldBackgroundColor: Color(0xFFF5F6FA),
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
),
),
💡 For advanced theming, consider using Google Fonts or a design system package like flutter_neumorphic or flutter_bloc + theming.
Run on Android Device
1. Make sure the Android device is connected:
adb devices
List of devices attached
10608373C5106067 device
2. Run the Flutter application:
flutter run
The Flutter Firebase app looks like this:
Final Recap and Conclusion
In this tutorial, you’ve built a fully functional and beautifully styled Flutter Firebase CRUD mobile app using Cloud Firestore as your backend. Here’s a summary of what we accomplished step-by-step:
🔧 What You Built:
- Firebase Setup
- Initialized Firebase in a Flutter project
- Configured Firestore as the database
- Clean Project Structure
- Separated code into models, services, and screens for better scalability
- Data Layer with Firestore Service
- Handled all Firestore operations (CRUD) in a dedicated service class
- Note Model
- Defined a reusable Note model to map Firestore documents to Dart objects
- Home Screen
- Displayed notes in a real-time list using StreamBuilder and ListView
- Included add, edit, and delete functionality
- Add/Edit Note Screen
- Created a form to add or update notes with form validation and clean UX
- Routing & Navigation
- Set up named routes and pass data between screens easily
- Modern UI Styling
- Applied padding, borders, elevation, and consistent theming for a polished user interface
🚀 What You Can Do Next
Now that your Flutter + Firebase app is working, here are a few ways to expand it:
- 🔍 Search and Filter Notes
- Let users quickly find notes with a search bar and filtering options.
- 🌓 Dark Mode Support
- Add dynamic theme switching based on user preference or system settings.
- 🏷️ Tags or Categories
- Group notes using tags and allow filtering by them.
- ☁️ User Authentication
- Let users log in and manage their own notes privately with Firebase Auth.
- 📦 Cloud Functions or Notifications
- Integrate with Firebase Cloud Functions for automation and push notifications for updates.
🎉 Conclusion
Congratulations! You've just created a multi-page, Firebase-powered Flutter CRUD mobile app with a real-time Firestore backend. This app serves as a solid foundation for more advanced Flutter applications, whether you're building productivity tools, note-taking apps, or admin panels.
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about Flutter, Dart, or related, you can take the following cheap course:
- Learn Flutter From Scratch
- Flutter, Beginner to Intermediate
- Flutter in 7 Days
- Learn Flutter from scratch
- Dart and Flutter: The Complete Developer's Guide
Thanks!