Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

06. Firebase

Previous | Main Page | Next

🔥 Setting Up Firebase Console for Your Flutter App

Before you can connect your Flutter app to Firebase, you need to set up your project on the Firebase Console. This process is very straightforward, you’ll only need a Google account.


🟢 Step 1: Log In to Firebase

To begin, open your web browser and head to the Firebase Console by visiting:

👉 https://console.firebase.google.com

Once the page loads, log in using your Google account. If you're already signed into Gmail or other Google services, you’ll probably be logged in automatically. Otherwise, just enter your email and password to continue.


🟢 Step 2: Create a New Firebase Project

After logging in, you’ll see the Firebase Dashboard. Find the button that says “Add project” or “Create project” and click on it to begin setting up your new project.

You’ll be asked to enter a name for your project — this can be anything you like, such as flutter-notes-app.

On the next screen, you might see options to enable Google Analytics and Gemini. For now, you can skip these by turning them off. Disabling Google Analytics will make the setup process quicker and simpler.


🟢 Step 3: Enable Firestore Database

Now that your project is created, it’s time to set up the database. In the left-hand sidebar, click on Build, and then select Firestore Database from the dropdown menu.

Once you’re on the Firestore page, you’ll see a button that says “Create database.” Click on it to begin.


🟢 Step 4: Choose the Firestore Location

Next, Firebase will ask you to choose a location for your Firestore database. This location determines where your data is stored. For the best performance, since we're in Southeast Asia, choose Singapore or Jakarta as the region.


🟢 Step 5: Set Security Rules for Firestore

Once your database is created, you’ll be taken to the Security Rules screen. Firebase will suggest using Production Mode, which means the database will start with stricter access by default. This is a good choice for safety, and you can change the rules later when needed.

To allow your app to start writing to the database during development, you’ll need to change the default rules. Click on the “Rules” tab, and you’ll see a block of code like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Here’s what to do:

  • Change if false to if true. This will temporarily allow full access while you're building and testing your app.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

After editing the rules, click “Publish” to save the changes. You might see a warning that this setting makes your database public. That’s okay for now while we’re still learning.


🤖 Connecting Flutter to Firebase (Android Studio Setup)

Now that you’ve finished setting up your Firebase project, it’s time to connect it to your Flutter app. We’ll do this in a few steps: installing the right tools, linking your Firebase project, and adding the necessary packages to your Flutter project.


🛠 Step 1: Install Firebase CLI & FlutterFire CLI

To get started, you’ll need to install some tools so that Flutter can connect to Firebase. Open Terminal inside your IDE, while opening your Flutter project. Type these commands one by one:

npm install -g firebase-tools

This installs Firebase CLI globally using Node.js. You only need to do this once.

Next, log into Firebase:

firebase login

After you run this, a browser window will pop up asking you to sign in to your Google account. This is the same account you used to set up your Firebase project earlier. Then, activate the FlutterFire CLI:

flutter pub global activate flutterfire_cli

And finally, link your Flutter app to the Firebase project using:

flutterfire configure

💡 Tip: When running flutterfire configure, it will ask you to choose a Firebase project from your account. Make sure to pick the same project you created in the Firebase Console.

Once this is done, FlutterFire will automatically generate a file called firebase_options.dart in your lib folder. This file contains the config info needed to initialize Firebase in your app.


📦 Step 2: Add Firebase Packages to Flutter

Now let’s add the necessary Firebase packages to your project.

In your terminal, run the following commands:

flutter pub add firebase_core
flutter pub add cloud_firestore

These two packages are essential:

  • firebase_core helps initialize Firebase in your Flutter app
  • cloud_firestore lets your app read/write data from Firestore

After adding them, also make sure to run:

flutter pub get

This will make sure the packages are properly downloaded and linked to your app.


⚠️ Step 3: Fix Android NDK Version (If You See an Error)

Sometimes, you might see an error related to the NDK version when you build the app on Android. If that happens, here’s how to fix it. Go to this file in your Flutter project:

android/build.gradle.kts

Or sometimes inside:

android/app/build.gradle.kts

Inside the android block, specify the NDK version like this:

android {
    ndkVersion = "27.0.12077973"
    
    // ... other settings
}

Save the file, and try running your app again.


🧩 Writing Simple CRUD with Flutter + Firebase

Once our project is connected to Firebase, we can start writing the actual code for the app. In this section, we’re going to build a simple note-taking app using Flutter and Firebase Firestore. The app will let users add, view, edit, and delete short notes, and all of this will be saved to the cloud using Firebase. Thanks to Firestore’s real-time database updates, changes should appear instantly.

This project is made up of three main Dart files:

  • main.dart → App entry point and Firebase setup
  • home_page.dart → The UI and logic for interacting with notes
  • firestore.dart → The service class that handles Firestore operations

🚀 main.dart: Setting Up the App

This is where your Flutter app starts running. The most important part here is initializing Firebase properly before rendering any UI.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}
  • WidgetsFlutterBinding.ensureInitialized(); makes sure that Flutter is ready before Firebase initializes.
  • Firebase.initializeApp(...) connects our app to Firebase using settings from firebase_options.dart (this file is auto-generated when you run flutterfire configure).
  • Then we launch the app with runApp.

The UI starts with this simple widget:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

We’re using HomePage() as the first screen, which is where our notes will show up.


🏠 home_page.dart: The Notes UI

This is the main page where users can add, update, and delete notes. Let’s look at the key parts.


🔗 Connecting to Firestore

First, we create an instance of our service class:

final FirestoreService firestoreService = FirestoreService();

We’ll use this to call addNote, updateNote, and deleteNote methods later.


✍️ The Add/Update Dialog

Whenever the user wants to add or update a note, we open a dialog box:

void openNoteBox({String? docID, String? existingText}) {
  textController.text = existingText ?? '';

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text(docID == null ? 'Add Note' : 'Update Note'),
      content: Form(
        key: _formKey,
        child: TextFormField(
          controller: textController,
          autofocus: true,
          decoration: InputDecoration(hintText: 'Enter your note here'),
          validator: (value) {
            if (value == null || value.trim().isEmpty) {
              return 'Please enter some text';
            }
            return null;
          },
        ),
      ),
      actions: [
        ElevatedButton(
          onPressed: () {
            if (_formKey.currentState!.validate()) {
              final text = textController.text.trim();
              Navigator.pop(context);

              // Decide whether to add or update
              if (docID == null) {
                firestoreService.addNote(text);
              } else {
                firestoreService.updateNote(docID, text);
              }
              // Reset the form
              textController.clear(); 
            }
          },
          child: Text(docID == null ? 'Add' : 'Update'),
        ),
      ],
    ),
  ).then((_) {
    // Reset if user taps outside dialog
    textController.clear(); 
  });
}

Here’s what it does:

  • Shows a dialog with a text field.
  • If it’s an update, it fills in the old note text.
  • When submitted, it either calls addNote() or updateNote() depending on whether a docID was passed.


📝 Displaying Notes

We use a StreamBuilder to automatically update the list of notes:

body: StreamBuilder(
  stream: firestoreService.getNotesStream(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      List notesList = snapshot.data!.docs;
  • getNotesStream() gives us live updates from Firestore.
  • Every time a note is added, updated, or deleted, the UI will refresh.

Inside the list builder:

return ListView.builder(
  itemCount: notesList.length,
  itemBuilder: (context, index) {
    DocumentSnapshot document = notesList[index];
    String docID = document.id;
    Map<String, dynamic> data = document.data() as Map<String, dynamic>;
    String noteText = data['note'];

We extract the note text and document ID so we can display it and know which one to update or delete. Each note looks like this:

return ListTile(
  title: Text(noteText),
  trailing: Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      IconButton(
        icon: Icon(Icons.settings),
        onPressed: () => openNoteBox(docID: docID, existingText: noteText),
      ),
      IconButton(
        icon: Icon(Icons.delete),
        onPressed: () => firestoreService.deleteNote(docID),
      ),
    ],
  ),
);

It simply consists of:

  • The note's text.
  • One button to edit.
  • One button to delete.

🔧 firestore.dart: Firebase Service Code

This file handles communication to Firestore, so we don’t clutter the UI code. Here's how your database should look like in the Firestore menu inside Firebase Console.

The Firestore Service contains 4 functionalities:

  • Add (Create)
  • Get (Read)
  • Update
  • Delete

📌 Add a Note

Future<void> addNote(String note) {
  return notes.add({
    'note': note,
    'timestamp': Timestamp.now(),
  });
}
  • Adds a new note to the notes collection.
  • Also stores the current timestamp so we can sort later.

🔁 Get Notes as a Stream

Stream<QuerySnapshot> getNotesStream() {
  return notes.orderBy('timestamp', descending: true).snapshots();
}
  • This gives us real-time updates whenever the data changes.

✏️ Update a Note

Future<void> updateNote(String docID, String newNote) {
  return notes.doc(docID).update({
    'note': newNote,
    'timestamp': Timestamp.now(),
  });
}
  • Updates the text and updates the timestamp (so the note moves to the top).

🗑 Delete a Note

Future<void> deleteNote(String docID) {
  return notes.doc(docID).delete();
}
  • Deletes the note with the given document ID.

Full Source Code

  • main.dart Dont forget to change the import:
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(),
    );
  }
}
  • homepage.dart
class HomePage extends StatefulWidget {
  const HomePage({super.key});

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

class _HomePageState extends State<HomePage> {
  final titleTextController = TextEditingController();
  final contentTextController = TextEditingController();

  final FirestoreService firestoreService = FirestoreService();

  void openNoteBox({String? docId, String? existingTitle, String? existingNote}) async {
    if (docId != null) {

      titleTextController.text = existingTitle ?? '';
      contentTextController.text = existingNote ?? '';

    }

    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text(docId == null ? "Create new Note" : "Edit Note"),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                decoration: InputDecoration(labelText: "Title"),
                controller: titleTextController,
              ),
              const SizedBox(height: 10),
              TextField(
                decoration: InputDecoration(labelText: "Content"),
                controller: contentTextController,
              ),
            ],
          ),
          actions: [
            MaterialButton(
              onPressed: () {
                if (docId == null) {
                  firestoreService.addNote(
                    titleTextController.text,
                    contentTextController.text,
                  );
                } else {
                  firestoreService.updateNote(
                    docId,
                    titleTextController.text,
                    contentTextController.text,
                  );
                }
                titleTextController.clear();
                contentTextController.clear();

                Navigator.pop(context);
              },
              child: Text(docId == null ? "Create" : "Update"),
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Notes")),
      floatingActionButton: FloatingActionButton(
        onPressed: openNoteBox,
        child: const Icon(Icons.add),
      ),
      body: StreamBuilder<QuerySnapshot>(
        stream: firestoreService.getNotes(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            List notesList = snapshot.data!.docs;

            return ListView.builder(
              itemCount: notesList.length,
              itemBuilder: (context, index) {
                DocumentSnapshot document = notesList[index];
                String docId = document.id;

                Map<String, dynamic> data =
                document.data() as Map<String, dynamic>;
                String noteTitle = data['title'];
                String noteContent = data['content'];

                return ListTile(
                  title: Text(noteTitle),
                  subtitle: Text(noteContent),
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: Icon(Icons.edit),
                        onPressed: () {
                          openNoteBox(docId: docId, existingNote: noteContent, existingTitle: noteTitle);
                        },
                      ),
                      IconButton(
                        icon: Icon(Icons.delete),
                        onPressed: () {
                          firestoreService.deleteNote(docId);
                        },
                      ),
                    ],
                  ),
                );
              },
            );
          } else {
            return const Text("No data");
          }
        },
      ),
    );
  }
}
  • firestore.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService{

  final CollectionReference notes = FirebaseFirestore.instance.collection('notes');

  //create new note
  Future<void> addNote(String title, String content) {
    return notes.add({
      'title': title,
      'content': content,
      'createdAt': Timestamp.now(),
    });
  }

  //fetch all notes
  Stream<QuerySnapshot> getNotes() {
    return notes.orderBy('createdAt', descending: true).snapshots();
  }

  //update notes
  Future<void> updateNote(String id, String title, String content) {
    return notes.doc(id).update({
      'title': title,
      'content': content,
      'createdAt': Timestamp.now(),
    });
  }

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

}