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
- Go to Firebase Console
- Click "Create a project"
- Enter project name:
personal-finance-tracker - Disable Google Analytics (we don't need it for this project)
- Click "Create project"
2.2 Enable Authentication
- In Firebase Console, go to "Authentication"
- Click "Get started"
- Go to "Sign-in method" tab
- Enable "Email/Password" provider
- Click "Save"
2.3 Create Firestore Database
- In Firebase Console, go to "Firestore Database"
- Click "Create database"
- Choose "Start in test mode" (we'll set up security rules later)
- Select a location close to your users (e.g., us-central1)
- Click "Done"
2.4 Add your flutter app to Firebase
- In Firebase Console, click "Add app" → Flutter
- Install the Firebase CLI:
npm install -g firebase-toolsif you have node installed or download the standalone version from the Firebase CLI page - Run
firebase loginin your terminal to log in to your Firebase account - 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
androidandios
- From any directory, run this command:
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 inrequest.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
- Sign up with a new email and password
- Sign in with existing credentials
- Sign out and verify you're redirected to login
- Try forgot password feature
9.2 Test Cloud Sync
- Add transactions and verify they appear instantly
- Delete transactions and see real-time updates
- Use multiple devices (or browsers) with the same account
- Go offline and verify app still works with local data
9.3 Test Security
- Try to access another user's data (should fail)
- Sign out and try to access data (should fail)
- 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
- Always authenticate users before accessing data
- Set up proper security rules to protect user data
- Handle offline scenarios gracefully
- Sync local and cloud data for best user experience
- Provide loading states for network operations
- 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!