CRUD Operation - Flutter , Firebase Auth & Firestore
Terence Faid JABO

Terence Faid JABO @faidterence

About: 📱 Mobile App & Fullstack Developer | 🖥️ Passionate about Coding 🚀 | 📚 Always Learning 🌟

Joined:
Aug 21, 2023

CRUD Operation - Flutter , Firebase Auth & Firestore

Publish Date: Mar 3
0 0

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

  1. Open your terminal.
  2. Run the command:

    flutter create simple_note_app
    
    
  3. Navigate into your project folder:

    cd simple_note_app
    
    
  4. Open the project in your code editor.

Step 2: Set Up Firebase

  1. Go to the Firebase Console.
  2. Click "Add Project" and follow the instructions.
  3. Add a new app to your project:
    • For mobile apps, select the Android/iOS option as needed.
  4. 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.

  1. 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

  1. Open terminal in your Flutter project.
  2. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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)),
      ),
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

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,
            ),
          ),
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode
  1. 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);
}

Enter fullscreen mode Exit fullscreen mode

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

  1. Open lib/main.dart.
  2. 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());
}

Enter fullscreen mode Exit fullscreen mode

Step 7 : Create the Login and Register Screen

  1. 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),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode
  1. 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),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

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);
  }
}


Enter fullscreen mode Exit fullscreen mode

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();
          }
        },
      ),
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

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();
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 2 : Integration

  1. 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"),
              ),
            ],
          ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

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"));
            }
          },
        ),
      ),
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

Comments 0 total

    Add comment