This guide walks you through building a basic note-taking app. Users can sign up or log in with their email and password and then add/view their own notes. The app uses Flutter for the UI, Firebase Authentication for user sign in/up, and Firestore to store notes.
Understanding
- Flutter is an open-source UI software development kit created by Google. It can be used to develop cross platform applications from a single codebase for the web, Android, iOS, Linux, macOS, and Windows.
- Firebase is a set of backend clouding computing services and application development platforms provided by Google.
Why Flutter over other options?
- It doesn't rely on web browser technology nor the set of widgets that ship with each device. Instead, Flutter uses its own high-performance rendering engine to draw widgets.
When to use Firebase ?
- Small to medium-sized projects
- Real-time Operations
- Authentication and User Management
When not use Firebase ?
- Complex Backend Logic
- Large-Scale Enterprise Applications
Requirements
- Flutter: Install Flutter by following the Flutter Installation Guide.
- Firebase Account: Sign up at the Firebase Console.
- Code Editor: Use VS Code, Android Studio, or any editor of your choice.
- Node: Make sure node is installed globally on your device.
Step 1: Create a New Flutter Project
- Open your terminal.
-
Run the command:
flutter create simple_note_app
-
Navigate into your project folder:
cd simple_note_app
Open the project in your code editor.
Step 2: Set Up Firebase
- Go to the Firebase Console.
- Click "Add Project" and follow the instructions.
-
Add a new app to your project:
- For mobile apps, select the Android/iOS option as needed.
-
Enable Authentication:
- In the Firebase Console, go to Authentication and enable the Email/Password sign-in method.
Step 3: Set Up Firebase & FlutterFire CLI Tools
These commands help you manage Firebase projects and configure your Flutter project to use Firebase.
- Install Firebase CLI Globally
npm install -g firebase-tools
This command installs the Firebase CLI globally using Node Package Manager (npm)
Log in to Firebase
firebase login
Activate FlutterFire CLI
dart pub global activate flutterfire_cli
This command activates the FlutterFire CLI globally using Dart's package manager.
Configure FlutterFire in Your Project
flutterfire configure
This command configures your Flutter project with your Firebase project. It generates the necessary configuration files and links your app to Firebase.
Step 4: Add Firebase Dependencies
- Open
terminal
in your Flutter project. -
Add these dependencies
flutter pub add firebase_core firebase_auth cloud_firestore
-
firebase_core:
This is the main package for connecting your app to Firebase. It initializes Firebase services and is required by other Firebase plugins.
-
firebase_auth:
This package allows you to add user authentication to your app.
-
cloud_firestore:
This package provides access to Cloud Firestore, a flexible, real-time NoSQL database from Firebase. It lets you store, sync, and retrieve user notes easily.
Step 5 : Create Re-usable compose
cd lib && mkdir components
- Text Input
import 'package:flutter/material.dart';
class Customtextfield extends StatelessWidget {
final String hintText;
final bool obscureText;
final TextEditingController textEditingController;
const Customtextfield({
super.key,
required this.hintText,
required this.obscureText,
required this.textEditingController,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: textEditingController,
obscureText: obscureText,
decoration: InputDecoration(
hintText: hintText,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
TextEditingController
in Flutter:
- Manages the text within a
TextField
. - Allows you to get, set, and listen to changes in that text.
- It is the tool that allows you to control the text field.
- Button
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
final String text;
final void Function() onPressed;
const CustomButton({super.key, required this.text, required this.onPressed});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: Container(
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(25),
child: Center(
child: Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}
- Message Util
import 'package:flutter/material.dart';
void displayMessageToUser(BuildContext context, String message, Color color) {
final snackBar = SnackBar(content: Text(message), backgroundColor: color);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
StatelessWidget:
- Use cases: Displaying static content, simple UI elements.
StatefulWidget:
- Use cases: Handling user interactions, displaying dynamic content, managing data.
Step 6: Initialize Firebase in Your App
- Open
lib/main.dart
. - Update the main function to:
import 'package:firebase_core/firebase_core.dart';
import 'package:project_name/firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
}
Step 7 : Create the Login and Register Screen
- Login
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_simple_auth/components/custom_button.dart';
import 'package:flutter_simple_auth/components/custom_text_field.dart';
import 'package:flutter_simple_auth/utils/display_message_user_util.dart';
class LoginScreen extends StatefulWidget {
final Function()? onTap;
const LoginScreen({super.key, this.onTap});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
void loginUser() async {
// show a loader
showDialog(
context: context,
builder: (context) => Center(child: CircularProgressIndicator()),
);
try {
if (passwordController.text.isEmpty || emailController.text.isEmpty) {
// pop loader
if (mounted) {
Navigator.pop(context);
}
// show error message
displayMessageToUser(context, "Please fill all fields", Colors.red);
return;
}
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: emailController.text,
password: passwordController.text,
);
// pop loader
if (!mounted) return;
Navigator.pop(context);
} on FirebaseAuthException catch (e) {
// pop loader
if (!mounted) return;
Navigator.pop(context);
// show error message
if (mounted) {
displayMessageToUser(
context,
e.message ?? "An error occurred",
Colors.red,
);
}
} catch (e) {
// pop loader
if (!mounted) return;
Navigator.pop(context);
// show error message
if (mounted) {
displayMessageToUser(context, e.toString(), Colors.red);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
spacing: 25,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person, size: 80),
// app name
Text("C O D E X", style: TextStyle(fontSize: 20)),
// email field
Customtextfield(
hintText: "Email",
obscureText: false,
textEditingController: emailController,
),
// Password
Customtextfield(
hintText: "Password",
obscureText: true,
textEditingController: passwordController,
),
CustomButton(text: "Login", onPressed: loginUser),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Text("Don't have an account?"),
GestureDetector(
onTap: widget.onTap,
child: Text(
"Sign Up",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
],
),
),
),
);
}
}
- Register Screen
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_simple_auth/components/custom_button.dart';
import 'package:flutter_simple_auth/components/custom_text_field.dart';
import 'package:flutter_simple_auth/utils/display_message_user_util.dart';
class RegisterScreen extends StatefulWidget {
final Function()? onTap;
const RegisterScreen({super.key, this.onTap});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final TextEditingController emailController = TextEditingController();
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController =
TextEditingController();
// function to register a user
void registerUser() async {
// show a loader
showDialog(
context: context,
builder: (context) => Center(child: CircularProgressIndicator()),
);
// check password match
if (passwordController.text != confirmPasswordController.text) {
// show error message
displayMessageToUser(context, "Passwords do not match", Colors.red);
} else {
// create user
try {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: emailController.text,
password: passwordController.text,
);
// pop loader
Navigator.pop(context);
} on FirebaseAuthException catch (e) {
// pop loader
Navigator.pop(context);
// show error message
displayMessageToUser(
context,
e.message ?? "An error occurred",
Colors.red,
);
} catch (e) {
// pop loader
Navigator.pop(context);
// show error message
displayMessageToUser(
context,
"An unexpected error occurred",
Colors.red,
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
spacing: 25,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person, size: 80),
// app name
Text("C O D E X", style: TextStyle(fontSize: 20)),
// email field
Customtextfield(
hintText: "Email",
obscureText: false,
textEditingController: emailController,
),
// username
Customtextfield(
hintText: "Username",
obscureText: false,
textEditingController: usernameController,
),
// Password
Customtextfield(
hintText: "Password",
obscureText: true,
textEditingController: passwordController,
),
// Confirm Password field
Customtextfield(
hintText: "Confirm Password",
obscureText: true,
textEditingController: confirmPasswordController,
),
CustomButton(text: "Register", onPressed: registerUser),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Text("""Already have an account?"""),
GestureDetector(
onTap: widget.onTap,
child: Text(
"Login",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
],
),
),
),
);
}
}
Step 8 :
The AuthHelper helps to toggle between Login and Register pages.
cd lib && mkdir auth && touch/auth_helper.dart
class AuthHelper extends StatefulWidget {
const AuthHelper({super.key});
@override
State<AuthHelper> createState() => _AuthHelperState();
}
class _AuthHelperState extends State<AuthHelper> {
// initially show login page
bool showLogin = true;
// function to toggle between login and register page
void toggleView() {
setState(() {
showLogin = !showLogin;
});
}
@override
Widget build(BuildContext context) {
return showLogin
? LoginScreen(onTap: toggleView)
: RegisterScreen(onTap: toggleView);
}
}
Step 9 :
cd lib/auth && touch/firebase_navigator_helper.dart
class FirebaseNavigatorHelper extends StatelessWidget {
const FirebaseNavigatorHelper({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasData) {
return WelcomeScreen();
} else if (snapshot.hasError) {
return const Center(child: Text("An error occurred"));
} else {
return AuthHelper();
}
},
),
);
}
}
PART TWO :
Notes CRUD Operation
Step 1 :
cd lib && mkdir services && touch firestore_notes_service.dart
class FirestoreNotesServices {
// get collection for notes
final CollectionReference notesCollection = FirebaseFirestore.instance
.collection('notes');
final currentUser = FirebaseAuth.instance.currentUser;
// CREATE with logged in user
Future<void> createNote(String note) {
return notesCollection.add({
'note': note,
'ownerId': currentUser?.uid,
'createdAt': Timestamp.now(),
});
}
// retrieve notes for logged in user
Stream<QuerySnapshot> getNotes() {
return notesCollection
.where('ownerId', isEqualTo: currentUser?.uid)
.snapshots();
}
// Update note
Future<void> updateNote(String note, String id) {
return notesCollection.doc(id).update({'note': note});
}
// Delete note
Future<void> deleteNotes(String id) {
return notesCollection.doc(id).delete();
}
}
Step 2 : Integration
-
cd lib/components && touch note_list.dart
class NotesList extends StatelessWidget {
final List notes;
final FirestoreNotesServices firestoreNotesServices;
final Function(String noteId, String currentNote) onUpdateNote;
const NotesList({
super.key,
required this.notes,
required this.firestoreNotesServices,
required this.onUpdateNote,
});
@override
Widget build(BuildContext context) {
if (notes.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.note_add, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
"No notes found",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
"Tap the + button to add your first note!",
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
);
}
return ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
return NoteCard(
note: notes[index]['note'],
noteId: notes[index].id,
firestoreNotesServices: firestoreNotesServices,
onUpdateNote: onUpdateNote,
);
},
);
}
}
class NoteCard extends StatelessWidget {
final String note;
final String noteId;
final FirestoreNotesServices firestoreNotesServices;
final Function(String noteId, String currentNote) onUpdateNote;
const NoteCard({
super.key,
required this.note,
required this.noteId,
required this.firestoreNotesServices,
required this.onUpdateNote,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
title: Text(note, style: const TextStyle(fontSize: 16)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => onUpdateNote(noteId, note),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
_showDeleteConfirmation(context);
},
),
],
),
onTap: () => onUpdateNote(noteId, note),
),
),
);
}
void _showDeleteConfirmation(BuildContext context) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Text("Delete Note"),
content: const Text("Are you sure you want to delete this note?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
"Cancel",
style: TextStyle(color: Colors.grey),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
firestoreNotesServices.deleteNotes(noteId);
Navigator.pop(context);
},
child: const Text("Delete"),
),
],
),
);
}
}
Lastly , create a home Screen
to display logged in user notes
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// note controller
final TextEditingController noteController = TextEditingController();
// firebase instance
final currentUser = FirebaseAuth.instance.currentUser;
final FirestoreNotesServices firestoreNotesServices =
FirestoreNotesServices();
void logout() {
FirebaseAuth.instance.signOut();
}
// open a dialog to add note
void addNoteDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Text(
"Add Note",
style: TextStyle(fontWeight: FontWeight.bold),
),
content: Customtextfield(
hintText: "Enter Note",
obscureText: false,
textEditingController: noteController,
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel", style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black87,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
firestoreNotesServices.createNote(noteController.text);
noteController.clear();
Navigator.pop(context);
},
child: const Text("Add"),
),
],
);
},
);
}
// Dialog for updating notes
void updateNoteDialog(String noteId, String currentNote) {
noteController.text = currentNote;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Text(
"Update Note",
style: TextStyle(fontWeight: FontWeight.bold),
),
content: Customtextfield(
hintText: "Enter Note",
obscureText: false,
textEditingController: noteController,
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel", style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black87,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
firestoreNotesServices.updateNote(noteController.text, noteId);
noteController.clear();
Navigator.pop(context);
},
child: const Text("Update"),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
backgroundColor: Colors.black87,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
title: const Text(
"Notes App",
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
onPressed: () {
logout();
},
icon: const Icon(Icons.logout),
tooltip: "Logout",
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
addNoteDialog();
},
backgroundColor: Colors.black87,
child: const Icon(Icons.add, color: Colors.white),
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: StreamBuilder(
stream: firestoreNotesServices.getNotes(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(color: Colors.black87),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 48,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
"Error: ${snapshot.error}",
style: const TextStyle(fontSize: 16),
),
],
),
);
}
// Check if we have data
if (snapshot.hasData) {
List notes = snapshot.data!.docs;
return NotesList(
notes: notes,
firestoreNotesServices: firestoreNotesServices,
onUpdateNote: updateNoteDialog,
);
} else {
return const Center(child: Text("No notes available"));
}
},
),
),
);
}
}