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 7: Local Storage - Personal Finance Tracker

Objectives

By the end of this lab, students will:

  • Understand what local storage is and why it's important
  • Learn how to use SQLite database in Flutter
  • Design a database schema for transactions
  • Implement CRUD operations (Create, Read, Update, Delete)
  • Store user preferences locally
  • Make data persist between app sessions

Prerequisites

  • Completed Lab 6 (navigation implementation)
  • Understanding of StatefulWidget and setState()
  • Familiarity with the Transaction model and app structure
  • Basic understanding of databases (helpful but not required)

1. Understanding Local Storage

1.1 What is Local Storage?

Local storage means saving data directly on the user's device (phone/tablet) so that:

  • Data survives when the app is closed and reopened
  • Data is available without internet connection
  • App loads faster because data is stored locally

1.2 Types of Local Storage in Flutter

1. SharedPreferences

  • For simple key-value pairs (settings, preferences)
  • Good for: user settings, theme preferences, simple flags
  • Not good for: complex data, lists of objects

2. SQLite Database

  • For complex, structured data with relationships
  • Good for: transactions, user data, anything that needs querying
  • Best choice for our finance tracker

3. File Storage

  • For files, images, documents
  • Good for: saving files, caching images
  • Not needed for our current app

1.3 Why SQLite for Our App?

Our finance tracker needs to:

  • Store many transactions (potentially hundreds)
  • Search and filter transactions
  • Sort transactions by date, amount, category
  • Calculate totals efficiently

SQLite is perfect for this because it's:

  • Fast for queries and calculations
  • Reliable and battle-tested
  • Built into Android and iOS
  • Easy to use with Flutter

2. Setting Up SQLite in Flutter

2.1 Add Required Dependencies

Add these packages to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  intl: ^0.19.0
  sqflite: ^2.3.0
  path: ^1.8.3
  shared_preferences: ^2.2.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

Run this command to install the packages:

flutter pub get

or run this command to add and install the packages:

flutter pub add intl sqflite path shared_preferences

2.2 Understanding the Packages

  • sqflite: SQLite database for Flutter
  • path: Helps us find the right location to store the database file
  • shared_preferences: For storing simple settings (theme, currency, etc.)

3. Database Design

3.1 Planning Our Database Schema

Before writing code, let's plan what data we need to store:

Transactions Table:

  • id (Primary Key) - Unique identifier
  • title - Transaction description
  • amount - Money amount (as number)
  • date - When the transaction happened
  • category - Food, Shopping, Salary, etc.
  • isExpense - True for expenses, False for income

Settings Table:

  • key - Setting name (like "currency", "theme")
  • value - Setting value (like "USD", "dark")

3.2 SQL Commands We'll Use

-- Create transactions table
CREATE TABLE transactions(
  id TEXT PRIMARY KEY,
  title TEXT NOT NULL,
  amount REAL NOT NULL,
  date TEXT NOT NULL,
  category TEXT NOT NULL,
  isExpense INTEGER NOT NULL
);

-- Create settings table
CREATE TABLE settings(
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

Note: SQLite doesn't have a boolean type, so we use INTEGER (0 = false, 1 = true).


4. Creating the Database Helper

4.1 Create the Database Helper Class

Create a new file lib/helpers/database_helper.dart:

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart'
    hide Transaction; // to avoid conflict with Transaction class
import '../models/transaction.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  static Database? _database;

  // Singleton pattern - ensures only one database connection
  factory DatabaseHelper() {
    return _instance;
  }

  DatabaseHelper._internal();

  // Get database instance
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  // Initialize the database
  Future<Database> _initDatabase() async {
    // Get the path to store the database
    String path = join(await getDatabasesPath(), 'finance_tracker.db');

    // Open the database and create tables if they don't exist
    return await openDatabase(path, version: 1, onCreate: _createTables);
  }

  // Create database tables
  Future<void> _createTables(Database db, int version) async {
    // Create transactions table
    await db.execute('''
      CREATE TABLE transactions(
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        amount REAL NOT NULL,
        date TEXT NOT NULL,
        category TEXT NOT NULL,
        isExpense INTEGER NOT NULL
      )
    ''');
    debugPrint('Database tables created successfully');
  }

  // CRUD Operations for Transactions

  // Create - Insert a new transaction
  Future<int> insertTransaction(Transaction transaction) async {
    final db = await database;

    final result = await db.insert('transactions', {
      'id': transaction.id,
      'title': transaction.title,
      'amount': transaction.amount,
      'date': transaction.date.toIso8601String(),
      'category': transaction.category,
      'isExpense': transaction.isExpense ? 1 : 0,
    }, conflictAlgorithm: ConflictAlgorithm.replace);

    debugPrint('Transaction inserted: ${transaction.title}');
    return result;
  }

  // Read - Get all transactions
  Future<List<Transaction>> getAllTransactions() async {
    final db = await database;

    final List<Map<String, dynamic>> maps = await db.query(
      'transactions',
      orderBy: 'date DESC', // Newest first
    );

    return List.generate(maps.length, (i) {
      return Transaction(
        id: maps[i]['id'],
        title: maps[i]['title'],
        amount: maps[i]['amount'],
        date: DateTime.parse(maps[i]['date']),
        category: maps[i]['category'],
        isExpense: maps[i]['isExpense'] == 1,
      );
    });
  }

  // Read - Get transactions by category
  Future<List<Transaction>> getTransactionsByCategory(String category) async {
    final db = await database;

    final List<Map<String, dynamic>> maps = await db.query(
      'transactions',
      where: 'category = ?',
      whereArgs: [category],
      orderBy: 'date DESC',
    );

    return List.generate(maps.length, (i) {
      return Transaction(
        id: maps[i]['id'],
        title: maps[i]['title'],
        amount: maps[i]['amount'],
        date: DateTime.parse(maps[i]['date']),
        category: maps[i]['category'],
        isExpense: maps[i]['isExpense'] == 1,
      );
    });
  }

  // Update - Modify an existing transaction
  Future<int> updateTransaction(Transaction transaction) async {
    final db = await database;

    final result = await db.update(
      'transactions',
      {
        'title': transaction.title,
        'amount': transaction.amount,
        'date': transaction.date.toIso8601String(),
        'category': transaction.category,
        'isExpense': transaction.isExpense ? 1 : 0,
      },
      where: 'id = ?',
      whereArgs: [transaction.id],
    );

    debugPrint('Transaction updated: ${transaction.title}');
    return result;
  }

  // Delete - Remove a transaction
  Future<int> deleteTransaction(String id) async {
    final db = await database;

    final result = await db.delete(
      'transactions',
      where: 'id = ?',
      whereArgs: [id],
    );

    debugPrint('Transaction deleted: $id');
    return result;
  }

  // Delete all data (for testing or reset)
  Future<void> deleteAllData() async {
    final db = await database;
    await db.delete('transactions');
    debugPrint('All data deleted');
  }

  // Close database connection
  Future<void> close() async {
    final db = await database;
    await db.close();
  }
}

4.2 Understanding the Database Helper

Key Concepts:

  1. Singleton Pattern: Only one database connection exists in the entire app
  2. Async/Await: Database operations take time, so we use async functions
  3. CRUD Operations: Create, Read, Update, Delete - the four basic database operations
  4. SQL Queries: We use SQL commands to interact with the database

Important Methods:

  • insertTransaction(): Adds a new transaction to the database
  • getAllTransactions(): Gets all transactions from the database
  • updateTransaction(): Modifies an existing transaction
  • deleteTransaction(): Removes a transaction from the database

5. Updating the App to Use Database

5.1 Create a Data Service

Create a new file lib/services/transaction_service.dart to manage all database operations:

import 'package:personal_finance_tracker/helpers/database_helper.dart';
import 'package:personal_finance_tracker/models/transaction.dart';

class TransactionService {
  final DatabaseHelper _databaseHelper = DatabaseHelper();

  // Get all transactions
  Future<List<Transaction>> getAllTransactions() async {
    return await _databaseHelper.getAllTransactions();
  }

  // Add a new transaction
  Future<void> addTransaction(Transaction transaction) async {
    await _databaseHelper.insertTransaction(transaction);
  }

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

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

  // Get transactions by category
  Future<List<Transaction>> getTransactionsByCategory(String category) async {
    return await _databaseHelper.getTransactionsByCategory(category);
  }

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

  // Calculate total expenses
  Future<double> getTotalExpenses() async {
    final transactions = await 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;
  }
}

5.2 Update the Main Screen to Use Database

Update your main.dart to use the database:

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

void main() {
  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 MainScreen(),
    );
  }
}

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();
  List<Transaction> _transactions = [];
  bool _isLoading = true;

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

  // Load transactions from database
  Future<void> _loadTransactions() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final transactions = await _transactionService.getAllTransactions();
      setState(() {
        _transactions = transactions;
        _isLoading = false;
      });
    } catch (e) {
      debugPrint('Error loading transactions: $e');
      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);
      await _loadTransactions(); // Reload to show the new transaction

      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);
      await _loadTransactions(); // Reload to update the list

      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')),
        );
      }
    }
  }

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

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

    return Scaffold(
      body: screens[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        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.settings),
            label: 'Settings',
          ),
        ],
      ),
    );
  }
}

5.3 Update the Dashboard Screen

Update your dashboard_screen.dart to handle the refresh functionality:

import 'package:flutter/material.dart';
import 'models/transaction.dart';
import 'screens/transaction_detail_screen.dart';
import 'widgets/add_transaction_form.dart';
import 'widgets/transaction_list.dart';

class DashboardScreen extends StatefulWidget {
  final List<Transaction> transactions;
  final Function(String, double, String, bool) onAddTransaction;
  final Function(String) onDeleteTransaction;
  final VoidCallback onRefresh;

  const DashboardScreen({
    super.key,
    required this.transactions,
    required this.onAddTransaction,
    required this.onDeleteTransaction,
    required this.onRefresh,
  });

  @override
  State<DashboardScreen> createState() => _DashboardScreenState();
}

class _DashboardScreenState extends State<DashboardScreen> {
  String? _selectedCategory;
  String _searchQuery = '';

  List<Transaction> get _filteredTransactions {
    var filtered = widget.transactions;

    if (_selectedCategory != null) {
      filtered = filtered
          .where((tx) => tx.category == _selectedCategory)
          .toList();
    }

    if (_searchQuery.isNotEmpty) {
      filtered = filtered
          .where(
            (tx) => tx.title.toLowerCase().contains(_searchQuery.toLowerCase()),
          )
          .toList();
    }

    return filtered;
  }

  void _startAddTransaction(BuildContext context) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (_) {
        return AddTransactionForm(onAddTransaction: widget.onAddTransaction);
      },
    );
  }

  void _navigateToTransactionDetail(Transaction transaction) async {
    final result = await Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => TransactionDetailScreen(transaction: transaction),
      ),
    );

    // If result is a transaction ID, it means user wants to delete it
    if (result != null && result is String) {
      widget.onDeleteTransaction(result);
    }
  }

  double get _totalIncome {
    return widget.transactions
        .where((tx) => !tx.isExpense)
        .fold(0.0, (sum, tx) => sum + tx.amount);
  }

  double get _totalExpenses {
    return widget.transactions
        .where((tx) => tx.isExpense)
        .fold(0.0, (sum, tx) => sum + tx.amount);
  }

  double get _balance {
    return _totalIncome - _totalExpenses;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Personal Finance Tracker'),
        backgroundColor: Theme.of(context).colorScheme.primary,
        foregroundColor: Theme.of(context).colorScheme.onPrimary,
        leading: const Icon(Icons.account_balance_wallet),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: widget.onRefresh,
            tooltip: 'Refresh',
          ),
        ],
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          widget.onRefresh();
        },
        child: SingleChildScrollView(
          physics: const AlwaysScrollableScrollPhysics(),
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Balance Card
              BalanceOverviewCard(balance: _balance),
              const SizedBox(height: 24),

              // Summary Cards
              Row(
                children: [
                  Expanded(
                    child: SummaryCard(
                      title: 'Income',
                      amount: _totalIncome,
                      icon: Icons.arrow_upward,
                      color: Colors.green,
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: SummaryCard(
                      title: 'Expenses',
                      amount: _totalExpenses,
                      icon: Icons.arrow_downward,
                      color: Colors.red,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 24),

              // Search Bar
              TextField(
                decoration: InputDecoration(
                  hintText: 'Search transactions...',
                  prefixIcon: const Icon(Icons.search),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                onChanged: (value) {
                  setState(() {
                    _searchQuery = value;
                  });
                },
              ),
              const SizedBox(height: 16),

              // Filter and Title
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Text(
                    'Recent Transactions',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                  DropdownButton<String?>(
                    hint: const Text('All'),
                    value: _selectedCategory,
                    onChanged: (newValue) {
                      setState(() {
                        _selectedCategory = newValue;
                      });
                    },
                    items: [
                      const DropdownMenuItem<String?>(
                        value: null,
                        child: Text('All Categories'),
                      ),
                      ...{...widget.transactions.map((tx) => tx.category)}.map(
                        (category) => DropdownMenuItem<String>(
                          value: category,
                          child: Text(category),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
              const SizedBox(height: 16),

              // Transaction List
              SizedBox(
                height: 400,
                child: TransactionList(
                  transactions: _filteredTransactions,
                  onDeleteTransaction: widget.onDeleteTransaction,
                  onTransactionTap: _navigateToTransactionDetail,
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _startAddTransaction(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}

// Keep the same BalanceOverviewCard and SummaryCard classes from Lab 6
class BalanceOverviewCard extends StatelessWidget {
  final double balance;

  const BalanceOverviewCard({super.key, required this.balance});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Container(
        width: double.infinity,
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Current Balance',
              style: TextStyle(fontSize: 16, color: Colors.grey),
            ),
            const SizedBox(height: 16),
            Text(
              '\$${balance.toStringAsFixed(2)}',
              style: TextStyle(
                fontSize: 36,
                fontWeight: FontWeight.bold,
                color: balance >= 0 ? Colors.green : Colors.red,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SummaryCard extends StatelessWidget {
  final String title;
  final double amount;
  final IconData icon;
  final Color color;

  const SummaryCard({
    super.key,
    required this.title,
    required this.amount,
    required this.icon,
    required this.color,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(icon, color: color),
                const SizedBox(width: 8),
                Text(
                  title,
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Text(
              '\$${amount.toStringAsFixed(2)}',
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }
}

6. Adding User Preferences with SharedPreferences

6.1 Create a Settings Service

Create a new file lib/services/settings_service.dart:

import 'package:shared_preferences/shared_preferences.dart';

class SettingsService {
  static const String _currencyKey = 'currency';
  static const String _darkModeKey = 'dark_mode';
  static const String _notificationsKey = 'notifications';

  // Get currency setting
  Future<String> getCurrency() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_currencyKey) ?? 'USD';
  }

  // Save currency setting
  Future<void> setCurrency(String currency) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_currencyKey, currency);
  }

  // Get dark mode setting
  Future<bool> getDarkMode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_darkModeKey) ?? false;
  }

  // Save dark mode setting
  Future<void> setDarkMode(bool isDarkMode) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_darkModeKey, isDarkMode);
  }

  // Get notifications setting
  Future<bool> getNotifications() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_notificationsKey) ?? true;
  }

  // Save notifications setting
  Future<void> setNotifications(bool enabled) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_notificationsKey, enabled);
  }

  // Clear all settings
  Future<void> clearAllSettings() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
  }
}

6.2 Update the Settings Screen

Update your settings_screen.dart to use the settings service:

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

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

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

class _SettingsScreenState extends State<SettingsScreen> {
  final SettingsService _settingsService = SettingsService();

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

  @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')));
    }
  }

  @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: const Icon(
                      Icons.person,
                      size: 30,
                      color: Colors.white,
                    ),
                  ),
                  const SizedBox(width: 16),
                  const Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'John Doe',
                        style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      Text(
                        'john.doe@example.com',
                        style: TextStyle(color: Colors.grey),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          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),

          // 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('Clear All Data'),
                  subtitle: const Text('Delete all transactions and settings'),
                  leading: const Icon(Icons.delete_forever, color: Colors.red),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: () {
                    _showClearDataDialog();
                  },
                ),
                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();
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  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 _showClearDataDialog() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Clear All Data'),
          content: const Text(
            'This will permanently delete all your transactions and settings. This action cannot be undone.',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Cancel'),
            ),
            ElevatedButton(
              onPressed: () async {
                // Clear all data
                await _settingsService.clearAllSettings();
                // You would also clear transaction data here

                if (context.mounted) {
                  Navigator.of(context).pop();

                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('All data cleared')),
                  );
                }
              },
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.red,
                foregroundColor: Colors.white,
              ),
              child: const Text('Clear All'),
            ),
          ],
        );
      },
    );
  }

  void _showAboutDialog() {
    showAboutDialog(
      context: context,
      applicationName: 'Personal Finance Tracker',
      applicationVersion: '1.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('Built with Flutter for educational purposes.'),
      ],
    );
  }
}

7. Testing the Database

7.1 Add Some Test Data

You can add a method to insert test data for development. Add this to your DatabaseHelper class:

// Add test data (for development only)
Future<void> insertTestData() async {
  final testTransactions = [
    Transaction(
      id: 'test1',
      title: 'Grocery Shopping',
      amount: 45.99,
      date: DateTime.now().subtract(const Duration(days: 1)),
      category: 'Food',
      isExpense: true,
    ),
    Transaction(
      id: 'test2',
      title: 'Monthly Salary',
      amount: 1500.00,
      date: DateTime.now().subtract(const Duration(days: 3)),
      category: 'Salary',
      isExpense: false,
    ),
    Transaction(
      id: 'test3',
      title: 'Coffee',
      amount: 4.50,
      date: DateTime.now(),
      category: 'Food',
      isExpense: true,
    ),
  ];

  for (var transaction in testTransactions) {
    await insertTransaction(transaction);
  }

  print('Test data inserted');
}

7.2 Add a Debug Button (Optional)

You can add a debug button to your settings screen to insert test data:

// Add this to the actions section in settings_screen.dart
ListTile(
  title: const Text('Add Test Data'),
  subtitle: const Text('Insert sample transactions (Debug only)'),
  leading: const Icon(Icons.bug_report),
  trailing: const Icon(Icons.arrow_forward_ios),
  onTap: () async {
    final dbHelper = DatabaseHelper();
    await dbHelper.insertTestData();

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Test data added!')),
      );
    }
  },
),

8. Key Concepts Summary

8.1 Database Concepts

  • SQLite: A lightweight database that runs on the device
  • Tables: Store data in rows and columns (like Excel sheets)
  • CRUD: Create, Read, Update, Delete - the four basic operations
  • Primary Key: A unique identifier for each row
  • SQL: The language used to interact with databases

8.2 Flutter Database Concepts

  • Async/Await: Database operations take time, so we use async functions
  • Singleton Pattern: Only one database connection in the entire app
  • Future: Represents a value that will be available in the future

8.3 Storage Types

  • SQLite: For complex, structured data (transactions, user data)
  • SharedPreferences: For simple settings (theme, currency, flags)
  • File Storage: For files, images, documents

9. Common Issues and Troubleshooting

9.1 Database Not Creating

Problem: Tables don't exist or data isn't saving Solution:

  • Check that _createTables() is being called
  • Verify SQL syntax in CREATE TABLE statements
  • Check database version number

9.2 Data Not Loading

Problem: App shows empty list even after adding transactions Solution:

  • Check that _loadTransactions() is called in initState()
  • Verify async/await usage
  • Check for errors in console output

9.3 App Crashes on Database Operations

Problem: App crashes when adding/deleting transactions Solution:

  • Wrap database operations in try-catch blocks
  • Check that all required fields are provided
  • Verify data types match the database schema

9.4 Settings Not Persisting

Problem: Settings reset when app is restarted Solution:

  • Check that SharedPreferences is properly initialized
  • Verify that settings are loaded in initState()
  • Make sure settings are saved when changed

10. Exercises

Exercise 1: Add Transaction Categories Management

Create a way to add/remove custom categories:

// Add to DatabaseHelper
Future<List<String>> getAllCategories() async {
  final db = await database;
  final result = await db.query(
    'transactions',
    columns: ['category'],
    distinct: true,
  );

  return result.map((row) => row['category'] as String).toList();
}

Exercise 2: Add Data Export

Implement a feature to export transactions to a text file:

// Add to TransactionService
Future<String> exportTransactionsToText() async {
  final transactions = await getAllTransactions();
  final buffer = StringBuffer();

  buffer.writeln('Personal Finance Tracker Export');
  buffer.writeln('Generated: ${DateTime.now()}');
  buffer.writeln('');

  for (var tx in transactions) {
    buffer.writeln('${tx.date.toIso8601String()},${tx.title},${tx.amount},${tx.category},${tx.isExpense ? 'Expense' : 'Income'}');
  }

  return buffer.toString();
}

Exercise 3: Add Monthly Summary

Create a method to get transactions for a specific month:

// Add to DatabaseHelper
Future<List<Transaction>> getTransactionsByMonth(int year, int month) async {
  final db = await database;
  final startDate = DateTime(year, month, 1);
  final endDate = DateTime(year, month + 1, 0);

  final List<Map<String, dynamic>> maps = await db.query(
    'transactions',
    where: 'date >= ? AND date <= ?',
    whereArgs: [startDate.toIso8601String(), endDate.toIso8601String()],
    orderBy: 'date DESC',
  );

  return List.generate(maps.length, (i) {
    return Transaction(
      id: maps[i]['id'],
      title: maps[i]['title'],
      amount: maps[i]['amount'],
      date: DateTime.parse(maps[i]['date']),
      category: maps[i]['category'],
      isExpense: maps[i]['isExpense'] == 1,
    );
  });
}

11. Key Takeaways

  • Local storage makes your app work offline and remember data
  • SQLite is perfect for complex data like transactions
  • SharedPreferences is great for simple settings
  • Always use async/await for database operations
  • Handle errors with try-catch blocks
  • Test your database operations thoroughly
  • Use services to organize your database code

Next Steps

In the next lab, we'll integrate RESTful APIs to add features like:

  • Currency conversion using live exchange rates
  • Financial tips from external sources
  • Loading indicators and error handling for network requests

Your app now has persistent storage - transactions and settings will survive app restarts! This is a major milestone in building a real-world app.