DEV Community

Faid T
Faid T

Posted on Edited on

CRUD Operation - Flutter , Firebase Auth & Firestore

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

Top comments (0)