Flutter with Firebase Realtime Database

Flutter Firebase Realtime Database CRUD Tutorial (2026) | D.Tech Academic

Flutter Firebase Realtime Database CRUD Tutorial (2026)

Last Updated: | Author: Deep Singh

Firebase Realtime Database is one of the easiest ways to add a backend to a Flutter app. It syncs data instantly across all connected clients — no server code, no REST endpoints, just direct reads and writes.

In this tutorial, you'll build a small student management app that does all four CRUD operations: Create (add a student), Read (list all students), Update (edit a student's details), and Delete (remove a student). We'll use the firebase_database plugin and FirebaseAnimatedList for live-updating lists.

A quick heads-up: Firebase Realtime Database is still fully supported in 2026, but for most new projects, Cloud Firestore is the better choice — it has richer querying, better scaling, and more intuitive data modelling. Use Realtime Database when you need very low latency, simple key-value data, or you're maintaining an existing app. This tutorial focuses on Realtime Database because that's what the original post covers.

Before You Start

Make sure you have the following ready:

  • Flutter SDK installed (latest stable version recommended)
  • Android Studio or VS Code with the Flutter plugin
  • A Google account for Firebase Console
  • Basic knowledge of Dart, StatefulWidget, and FutureBuilder
Flutter Firebase CRUD app showing validation errors in the form
Form validation in the Firebase CRUD app
Flutter Firebase Realtime Database app with student records list
Adding a new record to the database

Setting Up Firebase

Add the plugin to your pubspec.yaml:

dependencies:
  firebase_core: ^3.6.0
  firebase_database: ^11.1.4

Note on versions: The plugin version ^6.1.2 used in the original tutorial is several years old and no longer compatible with recent Flutter versions. The versions above are current as of 2026.

Now open the Firebase Console and create a new project.

Creating a new Firebase project in Firebase Console
Creating a Firebase project

Register your Android app by adding the package name from your AndroidManifest.xml. Download the google-services.json file and place it inside android/app/.

Adding the Android package name in Firebase Console
Package name from AndroidManifest.xml

Finally, in the Firebase Console, go to Realtime Database → Create Database and start in test mode for development. You'll lock down the rules before shipping.

Adding the Gradle Configuration

Apply the Google Services plugin in android/app/build.gradle:

Applying Google Services plugin in android app build.gradle
Applying the Google Services plugin

Add the Google Services classpath in android/build.gradle:

Adding Google Services classpath in android build.gradle
Adding the classpath

Important: If you're using the newer Gradle plugins DSL (the modern approach in 2026), the classpath goes in your settings.gradle under pluginManagement, and you apply the plugin with id 'com.google.gms.google-services' in the app-level build.gradle. The legacy apply plugin: syntax still works but is deprecated.

The Full Code

Here's a complete CRUD example. It has a form for adding or updating records, and a live list of all students with edit and delete buttons on each row.

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_database/ui/firebase_animated_list.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MaterialApp(
    title: 'Firebase CRUD Tutorial',
    home: MyHomeWidget(),
  ));
}

class MyHomeWidget extends StatefulWidget {
  const MyHomeWidget({super.key});

  @override
  State<MyHomeWidget> createState() => _MyHomeWidgetState();
}

class _MyHomeWidgetState extends State<MyHomeWidget> {
  final DatabaseReference _databaseReference =
      FirebaseDatabase.instance.ref().child('student');

  final TextEditingController _name = TextEditingController();
  final TextEditingController _email = TextEditingController();
  final TextEditingController _mobile = TextEditingController();
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  String _editKey = '';

  @override
  void dispose() {
    _name.dispose();
    _email.dispose();
    _mobile.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase CRUD'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            // ---------- Form ----------
            Form(
              key: _formKey,
              child: Column(
                children: [
                  _buildField(_name, 'Name Here', Icons.person),
                  _buildField(_email, 'Email Here', Icons.email),
                  _buildField(_mobile, 'Mobile Here', Icons.call),
                  const SizedBox(height: 20),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 20),
                    child: SizedBox(
                      width: double.infinity,
                      child: ElevatedButton(
                        onPressed: () {
                          if (_formKey.currentState!.validate()) {
                            _insertOrUpdate();
                          }
                        },
                        child: Text(_editKey.isEmpty ? 'Add Me' : 'Update'),
                      ),
                    ),
                  ),
                ],
              ),
            ),

            const SizedBox(height: 20),

            // ---------- Live List ----------
            FirebaseAnimatedList(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              query: _databaseReference,
              itemBuilder: (context, snapshot, animation, index) {
                final data = snapshot.value as Map<dynamic, dynamic>;
                return Card(
                  margin: const EdgeInsets.symmetric(
                    horizontal: 16,
                    vertical: 6,
                  ),
                  child: ListTile(
                    title: Text(data['name'] ?? ''),
                    subtitle: Text(data['email'] ?? ''),
                    trailing: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        IconButton(
                          icon: const Icon(Icons.edit),
                          onPressed: () {
                            setState(() {
                              _editKey = snapshot.key ?? '';
                              _name.text = data['name'] ?? '';
                              _email.text = data['email'] ?? '';
                              _mobile.text = data['mobile'] ?? '';
                            });
                          },
                        ),
                        IconButton(
                          icon: const Icon(Icons.delete),
                          onPressed: () => _delete(snapshot.key),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildField(
    TextEditingController controller,
    String label,
    IconData icon,
  ) {
    return Container(
      margin: const EdgeInsets.only(left: 20, right: 20, top: 16),
      child: TextFormField(
        controller: controller,
        validator: (value) =>
            (value == null || value.isEmpty) ? 'Please enter $label' : null,
        decoration: InputDecoration(
          prefixIcon: Icon(icon),
          labelText: label,
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }

  Future<void> _insertOrUpdate() async {
    final student = {
      'name': _name.text.trim(),
      'email': _email.text.trim(),
      'mobile': _mobile.text.trim(),
    };

    if (_editKey.isEmpty) {
      // INSERT
      await _databaseReference.push().set(student);
      debugPrint('Record added');
    } else {
      // UPDATE
      await _databaseReference.child(_editKey).update(student);
      debugPrint('Record updated: $_editKey');
      setState(() {
        _editKey = '';
      });
    }

    _formKey.currentState!.reset();
    _name.clear();
    _email.clear();
    _mobile.clear();
  }

  Future<void> _delete(String? key) async {
    if (key == null) return;
    await _databaseReference.child(key).remove();
    debugPrint('Record deleted: $key');
  }
}

How the Code Works

Let's walk through the important parts:

1. Initialising Firebase

WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();

You must call Firebase.initializeApp() before any other Firebase call. In older Flutter versions you didn't need the WidgetsFlutterBinding.ensureInitialized() line, but it's required in current versions when main() is async.

2. Getting a Database Reference

FirebaseDatabase.instance.ref().child('student');

This creates a reference to the student node in your Realtime Database. Every subsequent operation — insert, update, delete — goes through this reference.

Heads-up: The older FirebaseDatabase.instance.reference() method used in the original tutorial has been removed. Use .ref() instead.

3. Inserting a Record

await _databaseReference.push().set(student);

push() generates a unique key (like -MxyzABC...) so multiple records don't collide. Then .set() writes the data at that key. This is the recommended way to add records to a list-like collection.

4. Updating a Record

await _databaseReference.child(_editKey).update(student);

update() merges the provided keys into the existing record. Fields you don't include are left untouched. If you want to replace the entire record, use set() instead.

5. Deleting a Record

await _databaseReference.child(key).remove();

remove() deletes the node at that key. You get the key from snapshot.key inside the list's item builder.

6. Live Updates with FirebaseAnimatedList

FirebaseAnimatedList listens to the query and rebuilds automatically whenever the data changes. Add a record on one device, and it appears on another within milliseconds. No manual refresh needed.

7. Form Validation

The _formKey.currentState!.validate() call runs the validator on every field. Only if all return null does it return true, and only then do we write to the database.

8. Edit Mode

When you tap the edit icon, we set _editKey to the record's key and pre-fill the form. The submit button's label switches from "Add Me" to "Update". Tapping it calls update() on the existing record instead of push(). After the update, _editKey is reset and the form clears.

Setting Up Database Rules

The default Realtime Database rules in test mode allow anyone to read and write your entire database. That's fine for learning, but you should change them before going live. Here's a basic rule set that requires authentication:

{
  "rules": {
    "student": {
      ".read": "auth != null",
      ".write": "auth != null"
    }
  }
}

This means only signed-in users can read or write. If you haven't added Firebase Authentication yet, you can instead restrict by field-level validation:

{
  "rules": {
    "student": {
      ".read": true,
      ".write": true,
      ".validate": "newData.hasChildren(['name', 'email', 'mobile'])"
    }
  }
}

This still allows anonymous writes but prevents malformed records from being saved. It's not secure for production, but it's a good middle ground while you're building.

Wrapping Up

You now know how to perform all four CRUD operations against Firebase Realtime Database from a Flutter app. The live list updates automatically, the form handles both inserts and updates, and the delete buttons remove records cleanly.

A few things to keep in mind as you build on this:

  • Consider Cloud Firestore for new projects. Realtime Database is great for simple, low-latency data, but Firestore offers better querying, structured documents, and simpler scaling. Google's own recommendation is Firestore for most new work.
  • Structure your data shallowly. Realtime Database loads entire subtrees at once. Deeply nested data gets expensive fast. Flatten where possible.
  • Secure your rules. Never ship with ".read": true, ".write": true. Use Firebase Authentication and rule-based access control.
  • Handle offline gracefully. Realtime Database caches data locally and syncs when the device reconnects. That's a feature — use it to build offline-first experiences.
  • Keep an eye on costs. Firebase's free Spark plan is generous, but bandwidth and storage can add up. Check the pricing page.

No comments: