Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lab 9: Firebase Integration - Personal Finance Tracker

Objectives

By the end of this lab, students will:

  • Understand what Firebase is and its benefits
  • Set up a Firebase project and connect it to Flutter
  • Implement user authentication (sign up, login, logout)
  • Store and sync transaction data in the cloud using Firestore
  • Handle real-time data updates
  • Create a user account system with profile management
  • Sync data between multiple devices
  • Handle offline/online scenarios

Prerequisites

  • Completed Lab 8 (RESTful APIs implementation)
  • Understanding of async/await and Future
  • Familiarity with the finance tracker structure
  • Google account (for Firebase console access)
  • Basic understanding of user authentication concepts

1. Understanding Firebase

1.1 What is Firebase?

Firebase is Google's platform for building mobile and web applications. Think of it as a backend-as-a-service - it provides all the server-side functionality you need without having to build and manage servers yourself.

1.2 Why Use Firebase?

Without Firebase:

  • You need to build your own server
  • Set up databases
  • Handle user authentication
  • Manage security
  • Scale infrastructure

With Firebase:

  • Everything is pre-built and managed by Google
  • Automatic scaling
  • Built-in security
  • Real-time updates
  • Works offline

1.3 Firebase Services We'll Use

1. Firebase Authentication

  • User sign up, login, logout
  • Password reset
  • Email verification
  • Different sign-in methods (email, Google, Facebook, etc.)

2. Cloud Firestore

  • NoSQL database in the cloud
  • Real-time data synchronization
  • Offline support
  • Automatic scaling

3. Firebase Security Rules

  • Control who can read/write data
  • Ensure users can only access their own data

2. Setting Up Firebase

2.1 Create a Firebase Project

  1. Go to Firebase Console
  2. Click "Create a project"
  3. Enter project name: personal-finance-tracker
  4. Disable Google Analytics (we don't need it for this project)
  5. Click "Create project"

2.2 Enable Authentication

  1. In Firebase Console, go to "Authentication"
  2. Click "Get started"
  3. Go to "Sign-in method" tab
  4. Enable "Email/Password" provider
  5. Click "Save"

2.3 Create Firestore Database

  1. In Firebase Console, go to "Firestore Database"
  2. Click "Create database"
  3. Choose "Start in test mode" (we'll set up security rules later)
  4. Select a location close to your users (e.g., us-central1)
  5. Click "Done"

2.4 Add your flutter app to Firebase

  1. In Firebase Console, click "Add app" → Flutter
  2. Install the Firebase CLI: npm install -g firebase-tools if you have node installed or download the standalone version from the Firebase CLI page
  3. Run firebase login in your terminal to log in to your Firebase account
  4. Install and run the FlutterFire CLI :
    • From any directory, run this command: dart pub global activate flutterfire_cli
    • Then, at the root of your Flutter project directory, run this command: flutterfire configure --project=<your-project-id>
    • Choose what platforms you want to deploy to (web, iOS, Android, etc.) choose android and ios

3. Adding Firebase to Flutter

3.1 Add Firebase Dependencies

Run:

flutter pub add firebase_core firebase_auth cloud_firestore

3.2 Initialize Firebase in Your App

Update your main.dart:

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:personal_finance_tracker/firebase_options.dart';
import 'screens/login_screen.dart';
import 'screens/main_screen.dart';

void main() async {
  // Ensure Flutter is initialized
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Personal Finance Tracker',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const AuthWrapper(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        // Show loading screen while checking authentication
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }

        // If user is signed in, show main app
        if (snapshot.hasData && snapshot.data != null) {
          return const MainScreen();
        }

        // If user is not signed in, show login screen
        return const LoginScreen();
      },
    );
  }
}

3.3 Understanding the Auth Wrapper

The AuthWrapper automatically:

  • Listens for authentication state changes
  • Shows login screen when user is not signed in
  • Shows main app when user is signed in
  • Handles loading state while checking authentication

4. Creating User Authentication

4.1 Create Authentication Service

Create lib/services/auth_service.dart:

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // Get current user
  User? get currentUser => _auth.currentUser;

  // Get user stream
  Stream<User?> get authStateChanges => _auth.authStateChanges();

  // Sign up with email and password
  Future<UserCredential?> signUp({
    required String email,
    required String password,
    required String displayName,
  }) async {
    try {
      // Create user account
      final credential = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );

      // Update display name
      await credential.user?.updateDisplayName(displayName);
      await credential.user?.reload();

      debugPrint('User signed up: ${credential.user?.email}');
      return credential;
    } on FirebaseAuthException catch (e) {
      debugPrint('Sign up error: ${e.code} - ${e.message}');
      throw _handleAuthError(e);
    } catch (e) {
      debugPrint('Unknown sign up error: $e');
      throw 'An unexpected error occurred. Please try again.';
    }
  }

  // Sign in with email and password
  Future<UserCredential?> signIn({
    required String email,
    required String password,
  }) async {
    try {
      final credential = await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );

      debugPrint('User signed in: ${credential.user?.email}');
      return credential;
    } on FirebaseAuthException catch (e) {
      debugPrint('Sign in error: ${e.code} - ${e.message}');
      throw _handleAuthError(e);
    } catch (e) {
      debugPrint('Unknown sign in error: $e');
      throw 'An unexpected error occurred. Please try again.';
    }
  }

  // Sign out
  Future<void> signOut() async {
    try {
      await _auth.signOut();
      debugPrint('User signed out');
    } catch (e) {
      debugPrint('Sign out error: $e');
      throw 'Failed to sign out. Please try again.';
    }
  }

  // Send password reset email
  Future<void> sendPasswordResetEmail(String email) async {
    try {
      await _auth.sendPasswordResetEmail(email: email);
      debugPrint('Password reset email sent to: $email');
    } on FirebaseAuthException catch (e) {
      debugPrint('Password reset error: ${e.code} - ${e.message}');
      throw _handleAuthError(e);
    } catch (e) {
      debugPrint('Unknown password reset error: $e');
      throw 'Failed to send password reset email. Please try again.';
    }
  }

  // Handle Firebase Auth errors
  String _handleAuthError(FirebaseAuthException e) {
    switch (e.code) {
      case 'user-not-found':
        return 'No user found with this email address.';
      case 'wrong-password':
        return 'Wrong password provided.';
      case 'email-already-in-use':
        return 'An account already exists with this email address.';
      case 'weak-password':
        return 'The password provided is too weak.';
      case 'invalid-email':
        return 'The email address is not valid.';
      case 'user-disabled':
        return 'This user account has been disabled.';
      case 'too-many-requests':
        return 'Too many requests. Please try again later.';
      case 'requires-recent-login':
        return 'This operation requires recent authentication. Please sign in again.';
      default:
        return e.message ?? 'An authentication error occurred.';
    }
  }
}

4.2 Create Login Screen

Create lib/screens/login_screen.dart:

import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import 'signup_screen.dart';

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

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  final _authService = AuthService();

  bool _isLoading = false;
  bool _isPasswordVisible = false;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _signIn() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() {
      _isLoading = true;
    });

    try {
      await _authService.signIn(
        email: _emailController.text.trim(),
        password: _passwordController.text,
      );

      // AuthWrapper will automatically navigate to main app
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(e.toString()),
            backgroundColor: Colors.red,
          ),
        );
      }
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  Future<void> _forgotPassword() async {
    final email = _emailController.text.trim();
    if (email.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please enter your email address first')),
      );
      return;
    }

    try {
      await _authService.sendPasswordResetEmail(email);
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Password reset email sent to $email'),
            backgroundColor: Colors.green,
          ),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(e.toString()),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 60),

              // App Logo and Title
              const Icon(
                Icons.account_balance_wallet,
                size: 80,
                color: Colors.deepPurple,
              ),
              const SizedBox(height: 16),
              const Text(
                'Personal Finance Tracker',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 8),
              const Text(
                'Sign in to manage your finances',
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.grey,
                ),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 60),

              // Login Form
              Form(
                key: _formKey,
                child: Column(
                  children: [
                    // Email Field
                    TextFormField(
                      controller: _emailController,
                      keyboardType: TextInputType.emailAddress,
                      decoration: InputDecoration(
                        labelText: 'Email',
                        hintText: 'Enter your email',
                        prefixIcon: const Icon(Icons.email),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter your email';
                        }
                        if (!value.contains('@')) {
                          return 'Please enter a valid email';
                        }
                        return null;
                      },
                    ),
                    const SizedBox(height: 16),

                    // Password Field
                    TextFormField(
                      controller: _passwordController,
                      obscureText: !_isPasswordVisible,
                      decoration: InputDecoration(
                        labelText: 'Password',
                        hintText: 'Enter your password',
                        prefixIcon: const Icon(Icons.lock),
                        suffixIcon: IconButton(
                          icon: Icon(
                            _isPasswordVisible
                                ? Icons.visibility
                                : Icons.visibility_off,
                          ),
                          onPressed: () {
                            setState(() {
                              _isPasswordVisible = !_isPasswordVisible;
                            });
                          },
                        ),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter your password';
                        }
                        if (value.length < 6) {
                          return 'Password must be at least 6 characters';
                        }
                        return null;
                      },
                    ),
                    const SizedBox(height: 24),

                    // Sign In Button
                    SizedBox(
                      width: double.infinity,
                      height: 50,
                      child: ElevatedButton(
                        onPressed: _isLoading ? null : _signIn,
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.deepPurple,
                          foregroundColor: Colors.white,
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(12),
                          ),
                        ),
                        child: _isLoading
                            ? const CircularProgressIndicator(
                                valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                              )
                            : const Text(
                                'Sign In',
                                style: TextStyle(fontSize: 16),
                              ),
                      ),
                    ),
                    const SizedBox(height: 16),

                    // Forgot Password Button
                    TextButton(
                      onPressed: _forgotPassword,
                      child: const Text('Forgot Password?'),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 60),

              // Sign Up Link
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text("Don't have an account? "),
                  TextButton(
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => const SignUpScreen(),
                        ),
                      );
                    },
                    child: const Text('Sign Up'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

4.3 Create Sign Up Screen

Create lib/screens/signup_screen.dart:

import 'package:flutter/material.dart';
import '../services/auth_service.dart';

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

  @override
  State<SignUpScreen> createState() => _SignUpScreenState();
}

class _SignUpScreenState extends State<SignUpScreen> {
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  final _authService = AuthService();

  bool _isLoading = false;
  bool _isPasswordVisible = false;
  bool _isConfirmPasswordVisible = false;

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    _confirmPasswordController.dispose();
    super.dispose();
  }

  Future<void> _signUp() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() {
      _isLoading = true;
    });

    try {
      await _authService.signUp(
        email: _emailController.text.trim(),
        password: _passwordController.text,
        displayName: _nameController.text.trim(),
      );

      // Show success message
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Account created successfully!'),
            backgroundColor: Colors.green,
          ),
        );

        // AuthWrapper will automatically navigate to main app
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(e.toString()),
            backgroundColor: Colors.red,
          ),
        );
      }
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Create Account'),
        backgroundColor: Colors.deepPurple,
        foregroundColor: Colors.white,
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 20),

              // Title
              const Text(
                'Join Personal Finance Tracker',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 8),
              const Text(
                'Create your account to start tracking your finances',
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.grey,
                ),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 40),

              // Sign Up Form
              Form(
                key: _formKey,
                child: Column(
                  children: [
                    // Name Field
                    TextFormField(
                      controller: _nameController,
                      decoration: InputDecoration(
                        labelText: 'Full Name',
                        hintText: 'Enter your full name',
                        prefixIcon: const Icon(Icons.person),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter your name';
                        }
                        if (value.length < 2) {
                          return 'Name must be at least 2 characters';
                        }
                        return null;
                      },
                    ),
                    const SizedBox(height: 16),

                    // Email Field
                    TextFormField(
                      controller: _emailController,
                      keyboardType: TextInputType.emailAddress,
                      decoration: InputDecoration(
                        labelText: 'Email',
                        hintText: 'Enter your email',
                        prefixIcon: const Icon(Icons.email),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter your email';
                        }
                        if (!value.contains('@')) {
                          return 'Please enter a valid email';
                        }
                        return null;
                      },
                    ),
                    const SizedBox(height: 16),

                    // Password Field
                    TextFormField(
                      controller: _passwordController,
                      obscureText: !_isPasswordVisible,
                      decoration: InputDecoration(
                        labelText: 'Password',
                        hintText: 'Enter your password',
                        prefixIcon: const Icon(Icons.lock),
                        suffixIcon: IconButton(
                          icon: Icon(
                            _isPasswordVisible
                                ? Icons.visibility
                                : Icons.visibility_off,
                          ),
                          onPressed: () {
                            setState(() {
                              _isPasswordVisible = !_isPasswordVisible;
                            });
                          },
                        ),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter your password';
                        }
                        if (value.length < 6) {
                          return 'Password must be at least 6 characters';
                        }
                        return null;
                      },
                    ),
                    const SizedBox(height: 16),

                    // Confirm Password Field
                    TextFormField(
                      controller: _confirmPasswordController,
                      obscureText: !_isConfirmPasswordVisible,
                      decoration: InputDecoration(
                        labelText: 'Confirm Password',
                        hintText: 'Confirm your password',
                        prefixIcon: const Icon(Icons.lock_outline),
                        suffixIcon: IconButton(
                          icon: Icon(
                            _isConfirmPasswordVisible
                                ? Icons.visibility
                                : Icons.visibility_off,
                          ),
                          onPressed: () {
                            setState(() {
                              _isConfirmPasswordVisible = !_isConfirmPasswordVisible;
                            });
                          },
                        ),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please confirm your password';
                        }
                        if (value != _passwordController.text) {
                          return 'Passwords do not match';
                        }
                        return null;
                      },
                    ),
                    const SizedBox(height: 24),

                    // Sign Up Button
                    SizedBox(
                      width: double.infinity,
                      height: 50,
                      child: ElevatedButton(
                        onPressed: _isLoading ? null : _signUp,
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.deepPurple,
                          foregroundColor: Colors.white,
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(12),
                          ),
                        ),
                        child: _isLoading
                            ? const CircularProgressIndicator(
                                valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                              )
                            : const Text(
                                'Create Account',
                                style: TextStyle(fontSize: 16),
                              ),
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 40),

              // Sign In Link
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text('Already have an account? '),
                  TextButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: const Text('Sign In'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

5. Integrating Cloud Firestore

5.1 Understanding Firestore Structure

Firestore is a NoSQL database that stores data in documents and collections:

users (collection)
├── user1_id (document)
│   ├── name: "John Doe"
│   ├── email: "john@example.com"
│   └── transactions (subcollection)
│       ├── transaction1_id (document)
│       │   ├── title: "Grocery"
│       │   ├── amount: 45.99
│       │   └── date: "2024-01-15"
│       └── transaction2_id (document)
│           ├── title: "Salary"
│           ├── amount: 1500.00
│           └── date: "2024-01-01"
└── user2_id (document)
    └── ...

5.2 Create Firestore Service

Create lib/services/firestore_service.dart:

import 'package:cloud_firestore/cloud_firestore.dart' hide Transaction;
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import '../models/transaction.dart';

class FirestoreService {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // Get current user ID
  String? get _userId => _auth.currentUser?.uid;

  // Get user's transactions collection reference
  CollectionReference? get _userTransactionsCollection {
    if (_userId == null) return null;
    return _firestore
        .collection('users')
        .doc(_userId)
        .collection('transactions');
  }

  // Create or update user profile
  Future<void> createUserProfile({
    required String uid,
    required String name,
    required String email,
  }) async {
    try {
      await _firestore.collection('users').doc(uid).set({
        'name': name,
        'email': email,
        'createdAt': FieldValue.serverTimestamp(),
        'lastLoginAt': FieldValue.serverTimestamp(),
      }, SetOptions(merge: true));

      debugPrint('User profile created/updated: $email');
    } catch (e) {
      debugPrint('Error creating user profile: $e');
      rethrow;
    }
  }

  // Add transaction to Firestore
  Future<void> addTransaction(Transaction transaction) async {
    if (_userTransactionsCollection == null) {
      throw Exception('User not authenticated');
    }

    try {
      await _userTransactionsCollection!.doc(transaction.id).set({
        'title': transaction.title,
        'amount': transaction.amount,
        'date': Timestamp.fromDate(transaction.date),
        'category': transaction.category,
        'isExpense': transaction.isExpense,
        'createdAt': FieldValue.serverTimestamp(),
      });

      debugPrint('Transaction added to Firestore: ${transaction.title}');
    } catch (e) {
      debugPrint('Error adding transaction to Firestore: $e');
      rethrow;
    }
  }

  // Get all transactions from Firestore
  Future<List<Transaction>> getAllTransactions() async {
    if (_userTransactionsCollection == null) {
      throw Exception('User not authenticated');
    }

    try {
      final querySnapshot = await _userTransactionsCollection!
          .orderBy('date', descending: true)
          .get();

      return querySnapshot.docs.map((doc) {
        final data = doc.data() as Map<String, dynamic>;
        return Transaction(
          id: doc.id,
          title: data['title'],
          amount: data['amount'].toDouble(),
          date: (data['date'] as Timestamp).toDate(),
          category: data['category'],
          isExpense: data['isExpense'],
        );
      }).toList();
    } catch (e) {
      debugPrint('Error getting transactions from Firestore: $e');
      rethrow;
    }
  }

  // Get real-time transaction stream
  Stream<List<Transaction>> getTransactionsStream() {
    if (_userTransactionsCollection == null) {
      return Stream.empty();
    }

    return _userTransactionsCollection!
        .orderBy('date', descending: true)
        .snapshots()
        .map((querySnapshot) {
          return querySnapshot.docs.map((doc) {
            final data = doc.data() as Map<String, dynamic>;
            return Transaction(
              id: doc.id,
              title: data['title'],
              amount: data['amount'].toDouble(),
              date: (data['date'] as Timestamp).toDate(),
              category: data['category'],
              isExpense: data['isExpense'],
            );
          }).toList();
        });
  }

  // Update transaction in Firestore
  Future<void> updateTransaction(Transaction transaction) async {
    if (_userTransactionsCollection == null) {
      throw Exception('User not authenticated');
    }

    try {
      await _userTransactionsCollection!.doc(transaction.id).update({
        'title': transaction.title,
        'amount': transaction.amount,
        'date': Timestamp.fromDate(transaction.date),
        'category': transaction.category,
        'isExpense': transaction.isExpense,
        'updatedAt': FieldValue.serverTimestamp(),
      });

      debugPrint('Transaction updated in Firestore: ${transaction.title}');
    } catch (e) {
      debugPrint('Error updating transaction in Firestore: $e');
      rethrow;
    }
  }

  // Delete transaction from Firestore
  Future<void> deleteTransaction(String transactionId) async {
    if (_userTransactionsCollection == null) {
      throw Exception('User not authenticated');
    }

    try {
      await _userTransactionsCollection!.doc(transactionId).delete();
      debugPrint('Transaction deleted from Firestore: $transactionId');
    } catch (e) {
      debugPrint('Error deleting transaction from Firestore: $e');
      rethrow;
    }
  }

  // Sync local transactions to Firestore
  Future<void> syncLocalTransactionsToFirestore(
    List<Transaction> localTransactions,
  ) async {
    if (_userTransactionsCollection == null) return;

    try {
      final batch = _firestore.batch();

      for (final transaction in localTransactions) {
        final docRef = _userTransactionsCollection!.doc(transaction.id);
        batch.set(docRef, {
          'title': transaction.title,
          'amount': transaction.amount,
          'date': Timestamp.fromDate(transaction.date),
          'category': transaction.category,
          'isExpense': transaction.isExpense,
          'syncedAt': FieldValue.serverTimestamp(),
        });
      }

      await batch.commit();
      debugPrint(
        'Local transactions synced to Firestore: ${localTransactions.length} items',
      );
    } catch (e) {
      debugPrint('Error syncing local transactions to Firestore: $e');
      rethrow;
    }
  }
}

5.3 Update Transaction Service to Sync with Firestore

Update your lib/services/transaction_service.dart:

import 'package:flutter/foundation.dart';
import '../helpers/database_helper.dart';
import '../models/transaction.dart';
import 'firestore_service.dart';

class TransactionService {
  final DatabaseHelper _databaseHelper = DatabaseHelper();
  final FirestoreService _firestoreService = FirestoreService();

  bool _isOnline = true; // You can implement proper connectivity checking

  // Get all transactions (with offline support)
  Future<List<Transaction>> getAllTransactions() async {
    try {
      if (_isOnline) {
        // Try to get from Firestore first
        final firestoreTransactions = await _firestoreService.getAllTransactions();

        // Update local database with Firestore data
        for (final transaction in firestoreTransactions) {
          await _databaseHelper.insertTransaction(transaction);
        }

        return firestoreTransactions;
      } else {
        // Fallback to local database
        return await _databaseHelper.getAllTransactions();
      }
    } catch (e) {
      debugPrint('Error getting transactions from Firestore, falling back to local: $e');
      // Fallback to local database if Firestore fails
      return await _databaseHelper.getAllTransactions();
    }
  }

  // Get real-time transaction stream
  Stream<List<Transaction>> getTransactionsStream() {
    try {
      return _firestoreService.getTransactionsStream();
    } catch (e) {
      debugPrint('Error getting transaction stream: $e');
      return Stream.empty();
    }
  }

  // Add a new transaction (sync to both local and cloud)
  Future<void> addTransaction(Transaction transaction) async {
    try {
      // Always save locally first
      await _databaseHelper.insertTransaction(transaction);

      // Try to sync to Firestore
      if (_isOnline) {
        await _firestoreService.addTransaction(transaction);
      }
    } catch (e) {
      debugPrint('Error adding transaction: $e');
      // Transaction is saved locally even if cloud sync fails
      rethrow;
    }
  }

  // Update an existing transaction
  Future<void> updateTransaction(Transaction transaction) async {
    try {
      // Update locally
      await _databaseHelper.updateTransaction(transaction);

      // Try to sync to Firestore
      if (_isOnline) {
        await _firestoreService.updateTransaction(transaction);
      }
    } catch (e) {
      debugPrint('Error updating transaction: $e');
      rethrow;
    }
  }

  // Delete a transaction
  Future<void> deleteTransaction(String id) async {
    try {
      // Delete locally
      await _databaseHelper.deleteTransaction(id);

      // Try to sync to Firestore
      if (_isOnline) {
        await _firestoreService.deleteTransaction(id);
      }
    } catch (e) {
      debugPrint('Error deleting transaction: $e');
      rethrow;
    }
  }

  // Sync local data to Firestore (useful when going online)
  Future<void> syncToCloud() async {
    try {
      final localTransactions = await _databaseHelper.getAllTransactions();
      await _firestoreService.syncLocalTransactionsToFirestore(localTransactions);
    } catch (e) {
      debugPrint('Error syncing to cloud: $e');
      rethrow;
    }
  }

  // Calculate total income (from local data for speed)
  Future<double> getTotalIncome() async {
    final transactions = await _databaseHelper.getAllTransactions();
    double total = 0.0;
    for (var tx in transactions) {
      if (!tx.isExpense) {
        total += tx.amount;
      }
    }
    return total;
  }

  // Calculate total expenses (from local data for speed)
  Future<double> getTotalExpenses() async {
    final transactions = await _databaseHelper.getAllTransactions();
    double total = 0.0;
    for (var tx in transactions) {
      if (tx.isExpense) {
        total += tx.amount;
      }
    }
    return total;
  }

  // Calculate balance
  Future<double> getBalance() async {
    final income = await getTotalIncome();
    final expenses = await getTotalExpenses();
    return income - expenses;
  }
}

6. Update Main Screen with Real-time Data

6.4 Create New Main Screen

Create lib/screens/main_screen.dart:

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../screens/currency_converter_screen.dart';
import '../screens/settings_screen.dart';
import '../screens/statistics_screen.dart';
import '../dashboard_screen.dart';
import '../models/transaction.dart';
import '../services/transaction_service.dart';
import '../services/firestore_service.dart';

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

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;
  final TransactionService _transactionService = TransactionService();
  final FirestoreService _firestoreService = FirestoreService();
  List<Transaction> _transactions = [];
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _initializeUser();
    _setupTransactionStream();
  }

  // Initialize user profile in Firestore
  Future<void> _initializeUser() async {
    try {
      final user = FirebaseAuth.instance.currentUser;
      if (user != null) {
        await _firestoreService.createUserProfile(
          uid: user.uid,
          name: user.displayName ?? 'User',
          email: user.email ?? '',
        );
      }
    } catch (e) {
      debugPrint('Error initializing user: $e');
    }
  }

  // Set up real-time transaction stream
  void _setupTransactionStream() {
    _transactionService.getTransactionsStream().listen(
      (transactions) {
        if (mounted) {
          setState(() {
            _transactions = transactions;
            _isLoading = false;
          });
        }
      },
      onError: (error) {
        debugPrint('Transaction stream error: $error');
        // Fallback to loading from local database
        _loadTransactionsFromLocal();
      },
    );
  }

  // Fallback to local data
  Future<void> _loadTransactionsFromLocal() async {
    try {
      final transactions = await _transactionService.getAllTransactions();
      if (mounted) {
        setState(() {
          _transactions = transactions;
          _isLoading = false;
        });
      }
    } catch (e) {
      debugPrint('Error loading transactions from local: $e');
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  // Add a new transaction
  Future<void> _addTransaction(
    String title,
    double amount,
    String category,
    bool isExpense,
  ) async {
    final newTransaction = Transaction(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: title,
      amount: amount,
      date: DateTime.now(),
      category: category,
      isExpense: isExpense,
    );

    try {
      await _transactionService.addTransaction(newTransaction);

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Transaction added successfully!')),
        );
      }
    } catch (e) {
      debugPrint('Error adding transaction: $e');
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Failed to add transaction')),
        );
      }
    }
  }

  // Delete a transaction
  Future<void> _deleteTransaction(String id) async {
    try {
      await _transactionService.deleteTransaction(id);

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Transaction deleted successfully!')),
        );
      }
    } catch (e) {
      debugPrint('Error deleting transaction: $e');
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Failed to delete transaction')),
        );
      }
    }
  }

  // Refresh data manually
  Future<void> _refreshData() async {
    try {
      await _transactionService.syncToCloud();
      // The stream will automatically update the UI
    } catch (e) {
      debugPrint('Error refreshing data: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 16),
              Text('Loading your data...'),
            ],
          ),
        ),
      );
    }

    final List<Widget> screens = [
      DashboardScreen(
        transactions: _transactions,
        onAddTransaction: _addTransaction,
        onDeleteTransaction: _deleteTransaction,
        onRefresh: _refreshData,
      ),
      StatisticsScreen(transactions: _transactions),
      const CurrencyConverterScreen(),
      const SettingsScreen(),
    ];

    return Scaffold(
      body: screens[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed, // Show all tabs
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(
            icon: Icon(Icons.bar_chart),
            label: 'Statistics',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.currency_exchange),
            label: 'Convert',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
      ),
    );
  }
}

7. Adding User Profile and Sign Out

7.1 Update Settings Screen with Firebase Features

Update your lib/screens/settings_screen.dart:

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../services/auth_service.dart';
import '../services/settings_service.dart';
import '../services/transaction_service.dart';

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

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  final AuthService _authService = AuthService();
  final SettingsService _settingsService = SettingsService();
  final TransactionService _transactionService = TransactionService();

  bool _darkMode = false;
  bool _notifications = true;
  String _currency = 'USD';
  bool _isLoading = true;
  bool _isSyncing = false;

  User? get _currentUser => FirebaseAuth.instance.currentUser;

  @override
  void initState() {
    super.initState();
    _loadSettings();
  }

  Future<void> _loadSettings() async {
    try {
      final currency = await _settingsService.getCurrency();
      final darkMode = await _settingsService.getDarkMode();
      final notifications = await _settingsService.getNotifications();

      setState(() {
        _currency = currency;
        _darkMode = darkMode;
        _notifications = notifications;
        _isLoading = false;
      });
    } catch (e) {
      debugPrint('Error loading settings: $e');
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _updateDarkMode(bool value) async {
    await _settingsService.setDarkMode(value);
    setState(() {
      _darkMode = value;
    });

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Dark mode ${value ? 'enabled' : 'disabled'}')),
      );
    }
  }

  Future<void> _updateNotifications(bool value) async {
    await _settingsService.setNotifications(value);
    setState(() {
      _notifications = value;
    });

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Notifications ${value ? 'enabled' : 'disabled'}'),
        ),
      );
    }
  }

  Future<void> _updateCurrency(String currency) async {
    await _settingsService.setCurrency(currency);
    setState(() {
      _currency = currency;
    });

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Currency changed to $currency')),
      );
    }
  }

  Future<void> _syncData() async {
    setState(() {
      _isSyncing = true;
    });

    try {
      await _transactionService.syncToCloud();

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Data synced successfully!'),
            backgroundColor: Colors.green,
          ),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Sync failed: ${e.toString()}'),
            backgroundColor: Colors.red,
          ),
        );
      }
    } finally {
      if (mounted) {
        setState(() {
          _isSyncing = false;
        });
      }
    }
  }

  Future<void> _signOut() async {
    final confirm = await showDialog<bool>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Sign Out'),
          content: const Text('Are you sure you want to sign out?'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(false),
              child: const Text('Cancel'),
            ),
            ElevatedButton(
              onPressed: () => Navigator.of(context).pop(true),
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.red,
                foregroundColor: Colors.white,
              ),
              child: const Text('Sign Out'),
            ),
          ],
        );
      },
    );

    if (confirm == true) {
      try {
        await _authService.signOut();
        // AuthWrapper will automatically navigate to login screen
      } catch (e) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Sign out failed: ${e.toString()}'),
              backgroundColor: Colors.red,
            ),
          );
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Settings'),
        backgroundColor: Theme.of(context).colorScheme.primary,
        foregroundColor: Theme.of(context).colorScheme.onPrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16.0),
        children: [
          // Profile Section
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Row(
                children: [
                  CircleAvatar(
                    radius: 30,
                    backgroundColor: Theme.of(context).colorScheme.primary,
                    child: Text(
                      _currentUser?.displayName?.substring(0, 1).toUpperCase() ?? 'U',
                      style: const TextStyle(
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          _currentUser?.displayName ?? 'User',
                          style: const TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Text(
                          _currentUser?.email ?? '',
                          style: const TextStyle(color: Colors.grey),
                        ),
                        const SizedBox(height: 4),
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 8,
                            vertical: 2,
                          ),
                          decoration: BoxDecoration(
                            color: Colors.green.withOpacity(0.1),
                            borderRadius: BorderRadius.circular(12),
                          ),
                          child: const Text(
                            'Synced',
                            style: TextStyle(
                              color: Colors.green,
                              fontSize: 12,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 24),

          // Preferences Section
          const Text(
            'Preferences',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),

          Card(
            child: Column(
              children: [
                SwitchListTile(
                  title: const Text('Dark Mode'),
                  subtitle: const Text('Enable dark theme'),
                  value: _darkMode,
                  onChanged: _updateDarkMode,
                  secondary: const Icon(Icons.dark_mode),
                ),
                const Divider(height: 1),
                SwitchListTile(
                  title: const Text('Notifications'),
                  subtitle: const Text('Receive transaction alerts'),
                  value: _notifications,
                  onChanged: _updateNotifications,
                  secondary: const Icon(Icons.notifications),
                ),
                const Divider(height: 1),
                ListTile(
                  title: const Text('Currency'),
                  subtitle: Text('Current: $_currency'),
                  leading: const Icon(Icons.attach_money),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: _showCurrencyDialog,
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          // Cloud Sync Section
          const Text(
            'Cloud & Sync',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),

          Card(
            child: Column(
              children: [
                ListTile(
                  title: const Text('Sync Data'),
                  subtitle: const Text('Upload local data to cloud'),
                  leading: _isSyncing
                      ? const SizedBox(
                          width: 24,
                          height: 24,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Icon(Icons.cloud_sync),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: _isSyncing ? null : _syncData,
                ),
                const Divider(height: 1),
                ListTile(
                  title: const Text('Account Info'),
                  subtitle: const Text('Manage your account settings'),
                  leading: const Icon(Icons.account_circle),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Account management coming soon!')),
                    );
                  },
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          // Actions Section
          const Text(
            'Actions',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),

          Card(
            child: Column(
              children: [
                ListTile(
                  title: const Text('Export Data'),
                  subtitle: const Text('Download your transaction data'),
                  leading: const Icon(Icons.download),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Export feature coming soon!')),
                    );
                  },
                ),
                const Divider(height: 1),
                ListTile(
                  title: const Text('About'),
                  subtitle: const Text('App version and info'),
                  leading: const Icon(Icons.info),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: _showAboutDialog,
                ),
                const Divider(height: 1),
                ListTile(
                  title: const Text(
                    'Sign Out',
                    style: TextStyle(color: Colors.red),
                  ),
                  subtitle: const Text('Sign out of your account'),
                  leading: const Icon(Icons.logout, color: Colors.red),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: _signOut,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  void _showCurrencyDialog() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Select Currency'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: ['USD', 'EUR', 'GBP', 'JPY'].map((currency) {
              return RadioListTile<String>(
                title: Text(currency),
                value: currency,
                groupValue: _currency,
                onChanged: (value) {
                  if (value != null) {
                    _updateCurrency(value);
                  }
                  Navigator.of(context).pop();
                },
              );
            }).toList(),
          ),
        );
      },
    );
  }

  void _showAboutDialog() {
    showAboutDialog(
      context: context,
      applicationName: 'Personal Finance Tracker',
      applicationVersion: '2.0.0',
      applicationIcon: const Icon(Icons.account_balance_wallet),
      children: [
        const Text('A simple app to track your personal finances.'),
        const SizedBox(height: 16),
        const Text('Now with cloud sync powered by Firebase!'),
      ],
    );
  }
}

8. Setting Up Firebase Security Rules

8.1 Understanding Security Rules

Security rules control who can read and write data in Firestore. By default, we started in "test mode" which allows anyone to read/write data. Now we need to secure it.

8.2 Configure Security Rules

Go to Firebase Console → Firestore Database → Rules and replace with:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Users can only access their own data
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;

      // Users can only access their own transactions
      match /transactions/{transactionId} {
        allow read, write: if request.auth != null && request.auth.uid == userId;
      }
    }
  }
}

8.3 Understanding the Rules

  • request.auth != null: User must be signed in
  • request.auth.uid == userId: User can only access their own data
  • Nested rules: Transaction rules inherit from user rules

9. Testing Firebase Integration

9.1 Test User Authentication

  1. Sign up with a new email and password
  2. Sign in with existing credentials
  3. Sign out and verify you're redirected to login
  4. Try forgot password feature

9.2 Test Cloud Sync

  1. Add transactions and verify they appear instantly
  2. Delete transactions and see real-time updates
  3. Use multiple devices (or browsers) with the same account
  4. Go offline and verify app still works with local data

9.3 Test Security

  1. Try to access another user's data (should fail)
  2. Sign out and try to access data (should fail)
  3. Test with invalid authentication tokens

10. Key Firebase Concepts Summary

10.1 Authentication

  • Sign up/Sign in: Create and authenticate users
  • Auth state changes: Listen for login/logout events
  • User management: Profile updates, password reset
  • Security: Only authenticated users can access their data

10.2 Firestore Database

  • Collections: Groups of documents (like folders)
  • Documents: Individual records with fields
  • Subcollections: Collections inside documents
  • Real-time: Changes sync automatically across devices

10.3 Security Rules

  • Authentication-based: Control access based on user login
  • Data validation: Ensure data meets requirements
  • Field-level control: Different permissions for different fields

10.4 Offline Support

  • Local caching: Data works offline automatically
  • Sync on reconnect: Changes upload when online
  • Conflict resolution: Firebase handles simultaneous changes

11. Common Issues and Troubleshooting

11.1 Authentication Issues

Problem: "User not found" or "Wrong password" Solution: Verify email/password, check Firebase console users

Problem: App crashes on auth operations Solution: Ensure Firebase is initialized before any auth calls

11.2 Firestore Permission Errors

Problem: "Insufficient permissions" when accessing data Solution: Check security rules and ensure user is authenticated

Problem: Data not syncing Solution: Verify internet connection and Firestore rules

11.3 Build Errors

Problem: "Duplicate class" or Gradle errors Solution: Check build.gradle files match the documentation

Problem: "Firebase not initialized" Solution: Ensure Firebase.initializeApp() is called in main()


12. Key Takeaways

12.1 Benefits of Firebase

  • No server management: Google handles all infrastructure
  • Real-time sync: Changes appear instantly across devices
  • Offline support: App works without internet connection
  • Authentication: Secure user system built-in
  • Scalability: Handles millions of users automatically

12.2 Best Practices

  1. Always authenticate users before accessing data
  2. Set up proper security rules to protect user data
  3. Handle offline scenarios gracefully
  4. Sync local and cloud data for best user experience
  5. Provide loading states for network operations
  6. Handle errors appropriately with user-friendly messages

12.3 Architecture Pattern

UI Layer (Screens/Widgets)
    ↕
Service Layer (TransactionService, AuthService)
    ↕
Data Layer (LocalDB, Firestore)

This separation makes your app:

  • Easier to test
  • More maintainable
  • Flexible for different data sources

Next Steps

In the final lab (Lab 10), we'll:

  • Polish the UI with better theming and animations
  • Add app icons and splash screens
  • Optimize performance and add testing
  • Prepare for deployment to app stores
  • Add final touches like error boundaries and analytics

Your app now has:

  • User authentication with sign up/login
  • Cloud database with real-time sync
  • Offline support with local caching
  • Multi-device sync across all user devices
  • Secure data with proper access controls

This makes your finance tracker a production-ready app that users can rely on to manage their financial data securely across all their devices!