Dart Tutorial

Flutter Lesson 53 of 83 5 min read

Firebase with Flutter: Authentication and Firestore

Get started with Firebase in Flutter: set up with FlutterFire, sign users in with Firebase Auth, and store live data in Cloud Firestore.

On this page

Local storage keeps data on one device. To sign users in and share data between devices and people, you need a backend. Firebase is Google’s backend platform, and it is the most common choice for Flutter apps because it needs no server code to get started.

ServiceProvides
AuthenticationSign-in with email, phone, Google, Apple and more
Cloud FirestoreA cloud database that pushes changes to the app live
Cloud StorageFile uploads such as photos
Cloud MessagingPush notifications
CrashlyticsCrash reports from users’ devices
AnalyticsUsage statistics
Remote ConfigChange app behaviour without a new release

Firebase has a free tier. Usage beyond it is billed, so set budget alerts.

Setup #

Firebase’s tools change regularly. Follow the official FlutterFire setup guide for the exact commands. The outline:

  1. Create a project in the Firebase console.
  2. Install the Firebase CLI and sign in.
  3. From your Flutter project, run:
dart pub global activate flutterfire_cli
flutterfire configure

This registers your Android, iOS and web apps and generates lib/firebase_options.dart.

  1. Add the packages you need:
flutter pub add firebase_core firebase_auth cloud_firestore
  1. Initialise Firebase before the app starts:
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

Authentication #

Enable “Email/Password” under Authentication in the console first.

import 'package:firebase_auth/firebase_auth.dart';

final auth = FirebaseAuth.instance;

Future<String?> signUp(String email, String password) async {
  try {
    await auth.createUserWithEmailAndPassword(email: email, password: password);
    return null; // success
  } on FirebaseAuthException catch (e) {
    return switch (e.code) {
      'email-already-in-use' => 'An account already exists for that email.',
      'weak-password' => 'Choose a stronger password.',
      'invalid-email' => 'That email address is not valid.',
      _ => 'Could not create the account. Try again.',
    };
  }
}

Future<String?> signIn(String email, String password) async {
  try {
    await auth.signInWithEmailAndPassword(email: email, password: password);
    return null;
  } on FirebaseAuthException {
    return 'Wrong email or password.';
  }
}

Future<void> signOut() => auth.signOut();

Showing the right screen #

authStateChanges() is a stream of the current user. It emits on sign-in, sign-out and at app start, since Firebase remembers the session.

class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Scaffold(body: Center(child: CircularProgressIndicator()));
        }
        return snapshot.hasData ? const HomePage() : const LoginPage();
      },
    );
  }
}

Make AuthGate the home of your MaterialApp. Signing in or out then switches screens with no navigation code.

Cloud Firestore #

Firestore stores documents (like JSON objects) in collections.

notes (collection)
  ├── abc123 (document)  { text: "Buy milk", done: false, ownerId: "u1", createdAt: ... }
  └── def456 (document)  { text: "Call Hari", done: true,  ownerId: "u1", createdAt: ... }
import 'package:cloud_firestore/cloud_firestore.dart';

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

// Create
Future<void> addNote(String text, String userId) => notes.add({
      'text': text,
      'done': false,
      'ownerId': userId,
      'createdAt': FieldValue.serverTimestamp(),
    });

// Update
Future<void> setDone(String id, bool done) => notes.doc(id).update({'done': done});

// Delete
Future<void> deleteNote(String id) => notes.doc(id).delete();

// Read once
Future<void> printNotes(String userId) async {
  final snapshot = await notes.where('ownerId', isEqualTo: userId).get();
  for (final doc in snapshot.docs) {
    debugPrint('${doc.id}: ${doc.data()}');
  }
}

Live updates #

snapshots() returns a stream. The UI updates by itself whenever the data changes, on this device or any other.

class NotesList extends StatelessWidget {
  const NotesList({super.key, required this.userId});
  final String userId;

  @override
  Widget build(BuildContext context) {
    final query = FirebaseFirestore.instance
        .collection('notes')
        .where('ownerId', isEqualTo: userId)
        .orderBy('createdAt', descending: true);

    return StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
      stream: query.snapshots(),
      builder: (context, snapshot) {
        if (snapshot.hasError) return const Center(child: Text('Could not load notes'));
        if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());

        final docs = snapshot.data!.docs;
        if (docs.isEmpty) return const Center(child: Text('No notes yet'));

        return ListView(
          children: [
            for (final doc in docs)
              CheckboxListTile(
                title: Text(doc.data()['text'] as String),
                value: doc.data()['done'] as bool? ?? false,
                onChanged: (v) => doc.reference.update({'done': v}),
              ),
          ],
        );
      },
    );
  }
}

In a real app, create the query stream once in a state class or state holder, as explained in StreamBuilder, and convert documents into model classes.

Security rules are not optional #

The app talks to the database directly, so anyone could read or change your data unless rules stop them. Rules are written in the Firebase console.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /notes/{noteId} {
      // Only the owner may read or change a note.
      allow read, update, delete: if request.auth != null
                                  && request.auth.uid == resource.data.ownerId;
      allow create: if request.auth != null
                    && request.auth.uid == request.resource.data.ownerId;
    }
  }
}

“Test mode” rules allow everything and expire after a month. Never ship an app with them.

Things to know before committing to Firebase #

  • Queries are limited. There are no joins. You design the data around the screens that read it.
  • Cost follows reads and writes. A list that re-reads a thousand documents on every open adds up. Paginate, and listen only to what is on screen.
  • Offline support is built in on mobile: writes are queued and sent when the connection returns.
  • Alternatives exist. Supabase offers a similar service on top of a SQL database, and Appwrite and PocketBase can be self-hosted. You can also call your own REST API, as in the networking chapter.

Try it yourself #

Build a shared shopping list. Users sign up with email and password. Each item belongs to the signed-in user, appears live in a list, can be ticked off and deleted. Write security rules so that users see only their own items, and test them in the console’s rules playground.

Practise in the playground Updated by Santosh Adhikari