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

Practical Implementation Guide

For Courses in the Faculty of Computer Science & Information Technology

Academic Year: 2025

Course: Mobile Application Development with Flutter Lab

Practical Instructor: Mohammad Al-aqua

Academic Degree: Bachelor's in Computer Science

Student: ................................................

Academic Number: ..............................................

Major: .................................................

Group: .................................................

Mobile Application Development with Flutter

Flutter Personal Finance Tracker Course Outline

Lab 1: Introduction to Flutter

  • Set up Flutter development environment
  • Create the initial app with AppBar and basic scaffold
  • Introduce "Hello World" and hot reload concepts
  • Project component: App shell with title "My Finance Tracker"

Lab 2: Widgets and Layout

  • Understand the basics layout widgets (Padding, Container, Column, Row, etc.)
  • Design the dashboard layout with cards for summary info
  • Implement balance overview section with static data
  • Project component: Main dashboard with balance, income/expense cards

Lab 3: Lists and UI Components

  • Create transaction list with ListTile widgets
  • Add transaction item UI with amount, date, category
  • Implement custom transaction card design
  • Project component: Scrollable transaction history list

Lab 4: State Management

  • Implement adding/removing transactions
  • Create transaction model class
  • Use setState for UI updates after transactions change
  • Project component: Working "Add Transaction" button with state updates

Lab 5: Forms and Input

  • Build transaction entry form with validation
  • Add category selection dropdown
  • Implement date picker integration
  • Project component: Complete transaction entry form with validation

Lab 6: Navigation

  • Create transaction detail screen
  • Implement bottom navigation bar for different views
  • Add settings/profile screen
  • Project component: Multi-screen navigation structure

Lab 7: Local Storage

  • Design SQLite database schema for transactions
  • Implement CRUD operations for transactions
  • Add persistent storage for user preferences
  • Project component: Full local data persistence

Lab 8: RESTful APIs

  • Integrate currency conversion API (https://frankfurter.dev/)
  • Implement loading indicators and error handling
  • Project component: Currency conversion feature

Lab 9: Firebase Integration

  • Add user authentication
  • Implement cloud backup of transactions
  • Create data synchronization between devices
  • Project component: User account system with cloud sync

Lab 10: Polish and Deployment

  • Implement theming and dark mode
  • Add app icons and splash screens
  • Optimize performance and run tests
  • Project component: Production-ready app with deployment preparation

Lab 1: Introduction to Flutter - Personal Finance Tracker

Objectives

  • Set up Flutter development environment
  • Create your first Flutter application
  • Understand basic Flutter concepts and hot reload
  • Build the initial shell of your finance tracker app

Prerequisites

  • Basic programming knowledge
  • Computer with Windows, macOS, or Linux
  • Minimum 8GB RAM recommended
  • At least 10GB of free disk space

Setup Instructions

1. Install Flutter SDK

For Windows:

  1. Download the Flutter SDK from flutter.dev/docs/get-started/install/windows
  2. Extract the zip file to a location like C:\src\flutter (avoid spaces in the path)
  3. Add Flutter to your PATH:
    • Search for "Environment Variables" in Windows search
    • Click "Edit the system environment variables"
    • Click "Environment Variables"
    • Under "System variables", find and select "Path"
    • Click "Edit" and add the path to flutter\bin directory
    • Click "OK" to save

For macOS:

  1. Download the Flutter SDK from flutter.dev/docs/get-started/install/macos
  2. Extract the file to a location like ~/development
  3. Add Flutter to your PATH:
    • Open Terminal
    • Run nano ~/.zshrc or nano ~/.bash_profile (depending on your shell)
    • Add export PATH="$PATH:[PATH_TO_FLUTTER_DIRECTORY]/flutter/bin"
    • Save and exit (Ctrl+X, then Y)
    • Run source ~/.zshrc or source ~/.bash_profile

For Linux:

  1. Download the Flutter SDK from flutter.dev/docs/get-started/install/linux
  2. Extract to a location like ~/development/flutter
  3. Add Flutter to your PATH:
    • Run nano ~/.bashrc
    • Add export PATH="$PATH:[PATH_TO_FLUTTER_DIRECTORY]/flutter/bin"
    • Save and exit (Ctrl+X, then Y)
    • Run source ~/.bashrc

2. Install an IDE

Choose one of the following:

  1. Download and install from code.visualstudio.com
  2. Open VS Code and install the Flutter extension:
    • Click on Extensions icon on the left sidebar
    • Search for "Flutter"
    • Click "Install" on the Flutter extension by Dart Code

Android Studio:

  1. Download and install from developer.android.com/studio
  2. Open Android Studio and install the Flutter plugin:
    • Go to Preferences/Settings > Plugins
    • Search for "Flutter"
    • Click "Install" and restart Android Studio when prompted

3. Set up an Emulator or Connect a Physical Device

Android Emulator:

  1. Open Android Studio
  2. Click on "AVD Manager" (Android Virtual Device Manager)
  3. Click "Create Virtual Device"
  4. Select a phone (like Pixel 8) and click "Next"
  5. Download a system image (recommend API 30 or newer) and click "Next"
  6. Name your emulator and click "Finish"

iOS Simulator (macOS only):

  1. Install Xcode from the App Store
  2. Open Xcode and accept the license agreement
  3. Install additional components if prompted
  4. Open Terminal and run open -a Simulator

Physical Device:

  • For Android: Enable Developer Options and USB Debugging on your device
  • For iOS: Register as an Apple Developer and set up your device in Xcode

4. Verify Installation

  1. Open Terminal or Command Prompt
  2. Run flutter doctor
  3. Address any issues that appear with red X marks
  4. When most items have green checkmarks, you're ready to proceed

Creating Your First Flutter App

1. Create a New Flutter Project

  1. Open Terminal or Command Prompt
  2. Navigate to your desired project location
  3. Run the following command:
    flutter create personal_finance_tracker
    
  4. Wait for the project to be created
  5. Navigate into the project directory:
    cd personal_finance_tracker
    

2. Explore the Project Structure

Open the project in your IDE and explore the key files:

  • lib/main.dart - Main entry point of your application
  • pubspec.yaml - Project configuration and dependencies
  • android/ and ios/ - Platform-specific code

3. Run the Default App

  1. Start your emulator or connect your physical device
  2. In Terminal/Command Prompt (in your project directory), run:
    flutter run
    
  3. Wait for the app to compile and launch
  4. You should see the default Flutter counter app

4. Understand Hot Reload

  1. With the app running, open lib/main.dart in your IDE
  2. Find the text 'Flutter Demo' and change it to 'My Finance Tracker'
  3. Save the file
  4. Notice how the app updates immediately without restarting - this is hot reload!
  5. Try changing the colors or counter text to experiment further

Building the Finance Tracker Shell

Now that you understand the basics, let's create the initial shell for your finance tracker app.

1. Clean the Default App

Replace the entire content of lib/main.dart with this simplified code:

import 'package:flutter/material.dart';

void main() {
  runApp(const PersonalFinanceApp());
}

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

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

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

  @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,
      ),
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.account_balance_wallet, size: 100, color: Colors.green),
            SizedBox(height: 20),
            Text(
              'Welcome to Your Finance Tracker!',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 10),
            Text(
              'Track your expenses and income',
              style: TextStyle(fontSize: 16, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

2. Save and Run the App

  1. Save the file
  2. If the app is already running, it will hot reload
  3. If not, run flutter run in the terminal

3. Understand the Code

Let's break down what this code does:

  • void main() - The entry point of the app that runs our main app widget

  • PersonalFinanceApp - A StatelessWidget that sets up the MaterialApp

    • MaterialApp - Provides the overall structure and theme
    • title - The title shown in recent apps on mobile devices
    • theme - The color scheme and visual properties
    • home - The main widget to display
  • DashboardScreen - A StatelessWidget that creates our main screen

    • Scaffold - Provides the basic material design layout
    • AppBar - The top bar with the app title
    • body - The main content area

Lab Exercises

Exercise 1: Customize the App Theme

Change the primary color to a color of your choice. Try different colors from the Colors class, such as Colors.blue, Colors.purple, or Colors.teal.

Exercise 2: Add an App Icon to the AppBar

Modify the AppBar to include an icon. Add this before the title:

leading: Icon(Icons.account_balance_wallet),

Exercise 3: Add a Simple Action Button

Add a floating action button that will eventually be used to add transactions:

floatingActionButton: FloatingActionButton(
  onPressed: () {
    // Will implement this in future labs
    debugPrint('Button pressed!');
  },
  tooltip: 'Add Transaction',
  child: Icon(Icons.add),
),

Add a bottom navigation bar that will be expanded in future labs:

bottomNavigationBar: BottomNavigationBar(
  items: [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'Home',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.pie_chart),
      label: 'Statistics',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.settings),
      label: 'Settings',
    ),
  ],
  currentIndex: 0,
  onTap: (index) {
    // Will implement navigation in future labs
    debugPrint('Tapped item $index');
  },
),

Deliverables

By the end of this lab, you should have:

  1. A working Flutter development environment
  2. A basic understanding of Flutter project structure
  3. Experience with hot reload
  4. A simple app shell for your finance tracker with:
    • Customized app title
    • Basic theme
    • App bar
    • Floating action button
    • Bottom navigation bar

Troubleshooting Common Issues

  1. Flutter Doctor Shows Errors: Follow the suggestions provided by the flutter doctor command to resolve each issue.

  2. App Won't Run: Ensure your emulator is running or device is connected and recognized by running flutter devices.

  3. Hot Reload Not Working: Make sure you're saving the file and that there are no syntax errors.

  4. Missing Dependencies: If you see errors about missing packages, run flutter pub get in your project directory.

Next Steps

In the next lab, we'll expand our app by adding widgets and layouts to create a proper dashboard for our finance tracker.

Lab 2: Widgets and Layout - Personal Finance Tracker

Objectives

  • Understand basic Flutter layout widgets
  • Design a dashboard layout with information cards
  • Implement a balance overview section
  • Create income and expense summary cards

Prerequisites

  • Completed Lab 1
  • Working Flutter development environment
  • Basic understanding of Dart syntax

Understanding Flutter Layout Widgets

1. Container Widget

The Container widget is a convenience widget that combines common painting, positioning, and sizing widgets.

Key Properties:

  • child: The widget inside this container
  • padding: Empty space inside the container around the child
  • margin: Empty space outside the container
  • decoration: Visual styling (borders, shadows, background color)
  • width and height: Size constraints
  • alignment: How to position the child inside the container

Example:

Container(
  width: 200,
  height: 100,
  margin: EdgeInsets.all(10),
  padding: EdgeInsets.all(15),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 5,
        offset: Offset(0, 2),
      ),
    ],
  ),
  child: Text('Balance'),
)

2. Row and Column Widgets

Row and Column are the primary layout widgets for horizontal and vertical arrangements.

Key Properties for Both:

  • children: List of widgets to display
  • mainAxisAlignment: How to align children along the main axis (horizontal for Row, vertical for Column)
  • crossAxisAlignment: How to align children along the cross axis (vertical for Row, horizontal for Column)
  • mainAxisSize: How much space to take along the main axis (min or max)

Row Example:

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text('Income'),
    Text('\$1,200'),
  ],
)

Column Example:

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('Expenses'),
    Text('\$800'),
    Text('Shopping, Food, Transport'),
  ],
)

3. Padding Widget

The Padding widget adds empty space around its child.

Key Properties:

  • padding: The amount of space to add
  • child: The widget to pad

Example:

Padding(
  padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  child: Text('Balance Overview'),
)

4. SizedBox Widget

The SizedBox widget is used to create fixed-size spaces or to constrain child widgets.

Key Properties:

  • width and height: Fixed dimensions
  • child: Optional widget to constrain

Example:

// As a spacer:
SizedBox(height: 20)

// To constrain a child:
SizedBox(
  width: double.infinity,
  height: 100,
  child: Card(child: Center(child: Text('Balance'))),
)

5. Card Widget

The Card widget creates a material design card with elevation and rounded corners.

Key Properties:

  • child: The content of the card
  • elevation: How high the card is raised (affects shadow)
  • shape: The shape of the card
  • color: Background color

Example:

Card(
  elevation: 4,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),
  ),
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Text('Card Content'),
  ),
)

6. Expanded and Flexible Widgets

These widgets help distribute available space among children of a Row or Column.

Expanded forces the child to take all available space:

Row(
  children: [
    Expanded(
      child: Container(color: Colors.red, height: 100),
    ),
    Expanded(
      child: Container(color: Colors.blue, height: 100),
    ),
  ],
)

Flexible allows the child to shrink below its ideal size if needed:

Row(
  children: [
    Flexible(
      flex: 2,  // Takes 2/3 of available space
      child: Container(color: Colors.red, height: 100),
    ),
    Flexible(
      flex: 1,  // Takes 1/3 of available space
      child: Container(color: Colors.blue, height: 100),
    ),
  ],
)

Implementing the Finance Dashboard

Let's build the finance dashboard step by step:

1. Update the lib/main.dart File

First, let's modify our DashboardScreen class:

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

  @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),
      ),
      body: const FinanceDashboard(),
      bottomNavigationBar: BottomNavigationBar(
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.pie_chart),
            label: 'Statistics',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
        currentIndex: 0,
        onTap: (index) {
          debugPrint('Tapped item $index');
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          debugPrint('Add transaction button pressed');
        },
        tooltip: 'Add Transaction',
        child: const Icon(Icons.add),
      ),
    );
  }
}

2. Create the Dashboard Layout

Now, let's add the FinanceDashboard widget below the DashboardScreen class:

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

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Header section
          const Text(
            'My Balance',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),

          // Balance Card
          BalanceOverviewCard(),
          const SizedBox(height: 24),

          // Income & Expenses Row
          const Text(
            'Summary',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),

          // Income and Expense cards
          Row(
            children: [
              Expanded(
                child: SummaryCard(
                  title: 'Income',
                  amount: 1250.00,
                  icon: Icons.arrow_upward,
                  color: Colors.green,
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: SummaryCard(
                  title: 'Expenses',
                  amount: 850.00,
                  icon: Icons.arrow_downward,
                  color: Colors.red,
                ),
              ),
            ],
          ),
          const SizedBox(height: 24),

          // Recent Transactions Header
          const Text(
            'Recent Transactions',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 8),

          // Placeholder for transactions (will implement in Lab 3)
          const Center(
            child: Padding(
              padding: EdgeInsets.all(32.0),
              child: Text(
                'Your transactions will appear here',
                style: TextStyle(color: Colors.grey),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

3. Create the Balance Overview Card

Add the BalanceOverviewCard widget below:

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

  @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: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  'Current Balance',
                  style: TextStyle(
                    fontSize: 16,
                    color: Colors.grey,
                  ),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 10,
                    vertical: 5,
                  ),
                  decoration: BoxDecoration(
                    color: Colors.green.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: const Row(
                    children: [
                      Icon(
                        Icons.arrow_upward,
                        size: 14,
                        color: Colors.green,
                      ),
                      SizedBox(width: 4),
                      Text(
                        '2.5%',
                        style: TextStyle(
                          color: Colors.green,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            const Text(
              '\$400.00',
              style: TextStyle(
                fontSize: 36,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                _buildBalanceDetail(
                  context,
                  Icons.calendar_today,
                  'This Month',
                  '\$1,250.00',
                ),
                _buildBalanceDetail(
                  context,
                  Icons.account_balance_wallet,
                  'This Year',
                  '\$12,500.00',
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBalanceDetail(
    BuildContext context,
    IconData icon,
    String label,
    String amount,
  ) {
    return Row(
      children: [
        Container(
          padding: const EdgeInsets.all(8),
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Icon(
            icon,
            color: Theme.of(context).colorScheme.primary,
            size: 20,
          ),
        ),
        const SizedBox(width: 8),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              label,
              style: const TextStyle(
                fontSize: 12,
                color: Colors.grey,
              ),
            ),
            Text(
              amount,
              style: const TextStyle(
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ],
    );
  }
}

4. Create the Summary Cards

Finally, add the SummaryCard widget:

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: [
                Container(
                  padding: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: color.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Icon(
                    icon,
                    color: color,
                    size: 20,
                  ),
                ),
                const SizedBox(width: 8),
                Text(
                  title,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Text(
              '\$${amount.toStringAsFixed(2)}',
              style: const TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 4),
            Text(
              'This month',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey.shade600,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Understanding the Implementation

Let's break down what we've built:

  1. SingleChildScrollView: Makes the content scrollable if it's larger than the screen
  2. Column and Row Combinations: Organizes widgets vertically and horizontally
  3. Card Widgets: Creates elevated material design cards for information display
  4. Expanded Widgets: Distributes space evenly in the income/expense row
  5. Container with Decoration: Adds styling to various elements
  6. SizedBox: Creates consistent spacing between UI elements

Our dashboard has three main sections:

  • A balance overview card at the top
  • Income and expense summary cards in the middle
  • A placeholder for recent transactions (to be implemented in Lab 3)

Exercises

Exercise 1: Add a Savings Goal Card

Create a new card below the income and expense cards to display a savings goal:

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

  @override
  Widget build(BuildContext context) {
    // Goal progress (70%)
    const double progress = 0.7;

    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Row(
              children: [
                Icon(Icons.flag, color: Colors.blue),
                SizedBox(width: 8),
                Text(
                  'Savings Goal',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            const Text(
              'New Laptop',
              style: TextStyle(fontSize: 14),
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('\$700 of \$1,000'),
                Text(
                  '${(progress * 100).toInt()}%',
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.blue,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            LinearProgressIndicator(
              value: progress,
              backgroundColor: Colors.grey.shade200,
              valueColor: AlwaysStoppedAnimation<Color>(
                Theme.of(context).colorScheme.primary,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Add this card to the dashboard by inserting it between the income/expense cards and the recent transactions header:

// After the income and expense row
const SizedBox(height: 16),
const SavingsGoalCard(),
const SizedBox(height: 24),

Exercise 2: Add Custom Styling to the Balance Card

Enhance the BalanceOverviewCard with a gradient background. Modify the card's container:

Card(
  elevation: 4,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  child: Container(
    width: double.infinity,
    padding: const EdgeInsets.all(20),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: [
          Theme.of(context).colorScheme.inversePrimary,
          Theme.of(context).colorScheme.primary.inversePrimary(0.7),
        ],
      ),
      borderRadius: BorderRadius.circular(16),
    ),
    child: Column(
      // ... (existing code)
    ),
  ),
)

Remember to update the text colors to white for better contrast:

const Text(
  'Current Balance',
  style: TextStyle(
    fontSize: 16,
    color: Colors.white,
  ),
),

Exercise 3: Add a Refresh Button

Add a refresh button to the AppBar that will eventually update the dashboard data:

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: () {
        debugPrint('Refresh button pressed');
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Refreshing data...')),
        );
      },
    ),
  ],
),

Exercises 4: Organize your code

  • Move the DashboardScreen widget to a new file named dashboard_screen.dart.
  • Move every widget in the lib/main.dart file to its own file in the lib/widgets/ directory.

Deliverables

By the end of this lab, you should have:

  1. A comprehensive understanding of Flutter layout widgets
  2. A functional dashboard with:
    • Balance overview card
    • Income and expense summary cards
    • Layout for recent transactions (placeholder)
    • Optional savings goal card

Troubleshooting Common Issues

  1. Layout Overflow Errors: If you see yellow/black stripes on the screen, it means a widget is trying to take more space than available. Use Expanded, Flexible, or constrain the widget's size.

  2. Text Overflow: When text is too long for its container, use overflow: TextOverflow.ellipsis in TextStyle to show "..." instead of error stripes.

  3. Card Padding Issues: Remember that Cards already have some built-in padding. If your layout looks off, check if you're adding unnecessary padding.

  4. Colors Not Showing: When using Theme.of(context), ensure it's not called in a constructor and only within build methods.

  5. Rendering Issues: If the UI doesn't update as expected, try using Flutter's "Hot Restart" instead of just "Hot Reload".

Next Steps

In the next lab, we'll implement the transaction list using ListTile widgets and create a custom transaction card design. We'll also learn about scrollable lists and more advanced UI components.

Lab 3: Lists and UI Components - Personal Finance Tracker

Objectives

  • Understand different types of ListView widgets
  • Create a transaction list using ListTile widgets
  • Implement a custom transaction card design
  • Build a scrollable transaction history list

Prerequisites

  • Completed Lab 2
  • Basic understanding of Flutter widgets and layouts
  • Familiarity with Dart classes and collections

Understanding ListView in Flutter

What is ListView?

ListView is a scrollable list of widgets arranged linearly. It's one of Flutter's most commonly used widgets for displaying a scrollable collection of children.

Types of ListViews

1. ListView

The basic ListView constructor creates a scrollable, linear array of widgets.

ListView(
  children: [
    ListTile(title: Text('Item 1')),
    ListTile(title: Text('Item 2')),
    ListTile(title: Text('Item 3')),
  ],
)

This approach is good for a small, fixed number of children.

2. ListView.builder

Creates a scrollable, linear array of widgets that are built on demand. This is more efficient for long lists because it only builds items that are currently visible.

ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
)

Use this when you have a large or infinite list.

3. ListView.separated

Similar to ListView.builder but allows you to specify a separator widget between each item.

ListView.separated(
  itemCount: 100,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
  separatorBuilder: (context, index) {
    return Divider();
  },
)

Good for lists where you need visual separation between items.

4. ListView.custom

Provides the most customization options, allowing you to define custom child model objects.

ListView.custom(
  childrenDelegate: SliverChildBuilderDelegate(
    (context, index) => ListTile(title: Text('Item $index')),
    childCount: 100,
  ),
)

Reserved for advanced use cases.

ListTile Widget

ListTile is a specialized row widget designed for items in a ListView.

Key Properties:

  • leading: Widget to display before the title
  • title: The primary content of the list item
  • subtitle: Additional text displayed below the title
  • trailing: Widget to display after the title
  • onTap: Callback function when the tile is tapped
  • dense: Whether to make the tile more compact
  • isThreeLine: Whether the subtitle should be displayed on a third line
ListTile(
  leading: Icon(Icons.shopping_cart),
  title: Text('Groceries'),
  subtitle: Text('March 15, 2023'),
  trailing: Text('\$45.00'),
  onTap: () {
    print('Tile tapped!');
  },
)

Implementing the Transaction List

Let's implement the transaction list for our finance tracker app:

1. Create a Transaction Model

First, let's create a model to represent a financial transaction. Create a new file lib/models/transaction.dart:

class Transaction {
  final String id;
  final String title;
  final double amount;
  final DateTime date;
  final String category;
  final bool isExpense;

  Transaction({
    required this.id,
    required this.title,
    required this.amount,
    required this.date,
    required this.category,
    required this.isExpense,
  });
}

2. Create a Transaction List Widget

Now, let's create a widget for the transaction list. Create a new file lib/widgets/transaction_list.dart:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // Add this package with: flutter pub add intl
import '../models/transaction.dart';

class TransactionList extends StatelessWidget {
  final List<Transaction> transactions;
  final Function(String) onDeleteTransaction;

  const TransactionList({
    super.key,
    required this.transactions,
    required this.onDeleteTransaction,
  });

  @override
  Widget build(BuildContext context) {
    if (transactions.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.receipt_long,
              size: 64,
              color: Colors.grey.shade400,
            ),
            const SizedBox(height: 16),
            Text(
              'No transactions yet!',
              style: TextStyle(
                fontSize: 18,
                color: Colors.grey.shade600,
              ),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      itemCount: transactions.length,
      itemBuilder: (ctx, index) {
        return TransactionCard(
          transaction: transactions[index],
          onDelete: onDeleteTransaction,
        );
      },
    );
  }
}

3. Implement a Custom Transaction Card

Next, let's create a custom card for displaying transactions. Add this class to the transaction_list.dart file:

class TransactionCard extends StatelessWidget {
  final Transaction transaction;
  final Function(String) onDelete;

  const TransactionCard({
    super.key,
    required this.transaction,
    required this.onDelete,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
      elevation: 2,
      child: ListTile(
        leading: CircleAvatar(
          radius: 24,
          backgroundColor: transaction.isExpense
              ? Colors.red.shade100
              : Colors.green.shade100,
          child: Icon(
            getCategoryIcon(transaction.category),
            color: transaction.isExpense ? Colors.red : Colors.green,
          ),
        ),
        title: Text(
          transaction.title,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
        subtitle: Text(
          '${transaction.category} • ${DateFormat.yMMMd().format(transaction.date)}',
        ),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              '${transaction.isExpense ? '-' : '+'}\$${transaction.amount.toStringAsFixed(2)}',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color: transaction.isExpense ? Colors.red : Colors.green,
              ),
            ),
            IconButton(
              icon: const Icon(Icons.delete_outline, color: Colors.grey),
              onPressed: () => onDelete(transaction.id),
            ),
          ],
        ),
      ),
    );
  }

  IconData getCategoryIcon(String category) {
    switch (category.toLowerCase()) {
      case 'food':
        return Icons.restaurant;
      case 'shopping':
        return Icons.shopping_bag;
      case 'transport':
        return Icons.directions_car;
      case 'entertainment':
        return Icons.movie;
      case 'utilities':
        return Icons.receipt;
      case 'salary':
        return Icons.work;
      case 'gift':
        return Icons.card_giftcard;
      default:
        return Icons.attach_money;
    }
  }
}

4. Update the Main Dashboard

Now let's update our dashboard to include the transaction list. Modify the FinanceDashboard class in main.dart:

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

  @override
  State<FinanceDashboard> createState() => _FinanceDashboardState();
}

class _FinanceDashboardState extends State<FinanceDashboard> {
  // Sample transaction data
  final List<Transaction> _transactions = [
    Transaction(
      id: 't1',
      title: 'Grocery Shopping',
      amount: 45.99,
      date: DateTime.now().subtract(const Duration(days: 1)),
      category: 'Food',
      isExpense: true,
    ),
    Transaction(
      id: 't2',
      title: 'Monthly Salary',
      amount: 1500.00,
      date: DateTime.now().subtract(const Duration(days: 3)),
      category: 'Salary',
      isExpense: false,
    ),
    Transaction(
      id: 't3',
      title: 'New Headphones',
      amount: 99.99,
      date: DateTime.now().subtract(const Duration(days: 5)),
      category: 'Shopping',
      isExpense: true,
    ),
    Transaction(
      id: 't4',
      title: 'Restaurant Dinner',
      amount: 35.50,
      date: DateTime.now().subtract(const Duration(days: 7)),
      category: 'Food',
      isExpense: true,
    ),
  ];

  void _deleteTransaction(String id) {
    setState(() {
      _transactions.removeWhere((tx) => tx.id == id);
    });
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Header section
          const Text(
            'My Balance',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),

          // Balance Card
          const BalanceOverviewCard(),
          const SizedBox(height: 24),

          // Income & Expenses Row
          const Text(
            'Summary',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),

          // Income and Expense cards
          const Row(
            children: [
              Expanded(
                child: SummaryCard(
                  title: 'Income',
                  amount: 1250.00,
                  icon: Icons.arrow_upward,
                  color: Colors.green,
                ),
              ),
              SizedBox(width: 16),
              Expanded(
                child: SummaryCard(
                  title: 'Expenses',
                  amount: 850.00,
                  icon: Icons.arrow_downward,
                  color: Colors.red,
                ),
              ),
            ],
          ),
          const SizedBox(height: 24),

          // Recent Transactions Header
          const Text(
            'Recent Transactions',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),

          // Transaction List
          SizedBox(
            height: 400, // Fixed height for the list
            child: TransactionList(
              transactions: _transactions,
              onDeleteTransaction: _deleteTransaction,
            ),
          ),
        ],
      ),
    );
  }
}

5. Add Grouped Transaction List (Advanced)

For a more polished look, let's create a version of the transaction list that groups transactions by date. Create a new file lib/widgets/grouped_transaction_list.dart:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/transaction.dart';

class GroupedTransactionList extends StatelessWidget {
  final List<Transaction> transactions;
  final Function(String) onDeleteTransaction;

  const GroupedTransactionList({
    super.key,
    required this.transactions,
    required this.onDeleteTransaction,
  });

  @override
  Widget build(BuildContext context) {
    // Sort transactions by date (newest first)
    final sortedTransactions = List.of(transactions)
      ..sort((a, b) => b.date.compareTo(a.date));

    // Group transactions by date
    final Map<String, List<Transaction>> groupedTransactions = {};

    for (var tx in sortedTransactions) {
      final dateKey = DateFormat.yMMMd().format(tx.date);
      if (!groupedTransactions.containsKey(dateKey)) {
        groupedTransactions[dateKey] = [];
      }
      groupedTransactions[dateKey]!.add(tx);
    }

    // Convert to list of date-transactions pairs
    final groupList = groupedTransactions.entries.toList();

    return ListView.builder(
      itemCount: groupList.length,
      itemBuilder: (ctx, index) {
        final dateKey = groupList[index].key;
        final dateTransactions = groupList[index].value;

        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Text(
                dateKey,
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  color: Colors.grey.shade700,
                ),
              ),
            ),
            ...dateTransactions.map((tx) {
              return TransactionCard(
                transaction: tx,
                onDelete: onDeleteTransaction,
              );
            }).toList(),
            const SizedBox(height: 8),
          ],
        );
      },
    );
  }
}

To use this grouped list, replace the TransactionList widget in the dashboard with:

SizedBox(
  height: 400,
  child: GroupedTransactionList(
    transactions: _transactions,
    onDeleteTransaction: _deleteTransaction,
  ),
),

Exercises

Exercise 1: Implement a Transaction Filter

Add a dropdown button to filter transactions by category:

  • Add this at the top of the _FinanceDashboardState class
String? _selectedCategory;

// Add this method to the class
List<Transaction> get _filteredTransactions {
  if (_selectedCategory == null) {
    return _transactions;
  }
  return _transactions.where((tx) => tx.category == _selectedCategory).toList();
}
  • Add this above the transaction list
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    const Text(
      'Recent Transactions',
      style: TextStyle(
        fontSize: 18,
        fontWeight: FontWeight.bold,
      ),
    ),
    DropdownButton<String?>(
      hint: const Text('All Categories'),
      value: _selectedCategory,
      onChanged: (newValue) {
        setState(() {
          _selectedCategory = newValue;
        });
      },
      items: [
        const DropdownMenuItem<String?>(
          value: null,
          child: Text('All Categories'),
        ),
        ...{..._transactions.map((tx) => tx.category)}.map(
          (category) => DropdownMenuItem<String>(
            value: category,
            child: Text(category),
          ),
        ),
      ],
    ),
  ],
),
  • Update the TransactionList to use filtered transactions
SizedBox(
  height: 400,
  child: TransactionList(
    transactions: _filteredTransactions,
    onDeleteTransaction: _deleteTransaction,
  ),
),

Add a search bar to filter transactions by title:

  • Add this to the _FinanceDashboardState class
String _searchQuery = '';
  • Update the _filteredTransactions getter
List<Transaction> get _filteredTransactions {
  var filtered = _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;
}
  • Add this above the dropdown filter
Padding(
  padding: const EdgeInsets.only(bottom: 16),
  child: TextField(
    decoration: InputDecoration(
      hintText: 'Search transactions...',
      prefixIcon: const Icon(Icons.search),
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
    ),
    onChanged: (value) {
      setState(() {
        _searchQuery = value;
      });
    },
  ),
),

Common Issues and Troubleshooting

  1. List items not showing: Ensure your ListView has a defined height or is inside a widget that constrains its height (like Expanded or SizedBox).

  2. Overflow errors in ListTile: ListTile has a fixed height. For content that may overflow, consider using a custom layout instead.

  3. Performance issues with long lists: Make sure you're using ListView.builder rather than the default ListView constructor for long lists.

  4. ListView inside Column causing errors: ListView tries to be infinitely tall inside a Column. Wrap it in a SizedBox with a fixed height or an Expanded widget.

  5. Items disappearing when scrolling: This might happen if your ListView.builder doesn't properly reuse widgets. Check your itemBuilder function.

Next Steps

In the next lab, we'll focus on state management techniques in Flutter. We'll learn how to properly manage the state of our app, implement adding and removing transactions, and ensure UI updates correctly reflect these changes.

Don't forget to add the intl package to your project by running:

flutter pub add intl

Lab 4: State Management - Personal Finance Tracker

Objectives

By the end of this lab, students will:

  • Understand the difference between Stateless and Stateful widgets
  • Learn how to use the setState() method to update the UI
  • Implement adding and removing transactions in the finance tracker
  • See how state changes are reflected in the app instantly

Prerequisites

  • Completed Lab 3 (transaction list implementation)
  • Basic understanding of Dart classes and Flutter widgets
  • Familiarity with the Transaction model from Lab 3

1. Understanding State in Flutter

In Flutter, state means data that can change over time.

  • If your widget never changes after it’s built → use a StatelessWidget
  • If your widget needs to change (e.g., after a button press, form submission, or API call) → use a StatefulWidget

1.1 StatelessWidget

A StatelessWidget is immutable — once it’s built, it cannot change its data.

Example:

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

  @override
  Widget build(BuildContext context) {
    return const Text('Hello, I never change!');
  }
}
  • The text "Hello, I never change!" will always be the same.
  • If you want to change it, you must rebuild the whole widget from outside.

1.2 StatefulWidget

A StatefulWidget can change its data while the app is running.

Example:

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

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int counter = 0;

  void _incrementCounter() {
    setState(() {
      counter++; // Change the state
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $counter'),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: const Text('Increase'),
        ),
      ],
    );
  }
}

Here’s what happens:

  1. The widget starts with counter = 0
  2. When the button is pressed, _incrementCounter() is called
  3. Inside _incrementCounter(), we call setState()
  4. setState() tells Flutter: "Hey, my data changed — rebuild the UI"
  5. Flutter rebuilds the widget with the new value

2. The setState() Method

setState() is the simplest way to update the UI in Flutter.

Syntax:

setState(() {
  // Change your variables here
});

Rules for using setState():

  • Only call it inside a StatefulWidget’s State class
  • Keep the code inside setState() short — just update variables
  • Flutter will rebuild only the widget where the state changed, not the whole app

3. Applying State Management to Our Finance Tracker

In Lab 3, we had a hardcoded list of transactions.
Now, we’ll make it dynamic — so students can add and remove transactions.


3.1 Updating the Dashboard to be Stateful

In main.dart, make sure the FinanceDashboard is a StatefulWidget:

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

  @override
  State<FinanceDashboard> createState() => _FinanceDashboardState();
}

3.2 Adding State Variables

Inside _FinanceDashboardState, we are storing our transactions in a list:

class _FinanceDashboardState extends State<FinanceDashboard> {
  final List<Transaction> _transactions = [
    Transaction(
      id: 't1',
      title: 'Grocery Shopping',
      amount: 45.99,
      date: DateTime.now().subtract(const Duration(days: 1)),
      category: 'Food',
      isExpense: true,
    ),
    Transaction(
      id: 't2',
      title: 'Monthly Salary',
      amount: 1500.00,
      date: DateTime.now().subtract(const Duration(days: 3)),
      category: 'Salary',
      isExpense: false,
    ),
    Transaction(
      id: 't3',
      title: 'New Headphones',
      amount: 99.99,
      date: DateTime.now().subtract(const Duration(days: 5)),
      category: 'Shopping',
      isExpense: true,
    ),
    Transaction(
      id: 't4',
      title: 'Restaurant Dinner',
      amount: 35.50,
      date: DateTime.now().subtract(const Duration(days: 7)),
      category: 'Food',
      isExpense: true,
    ),
  ];

3.3 Adding a New Transaction

We’ll create a method _addTransaction() in _FinanceDashboardState:

  void _addTransaction(String title, double amount, String category, bool isExpense) {
    final newTx = Transaction(
      id: DateTime.now().toString(),
      title: title,
      amount: amount,
      date: DateTime.now(),
      category: category,
      isExpense: isExpense,
    );

    setState(() {
      _transactions.add(newTx);
    });
  }

Note: We call setState() inside the method to update the UI as soon as we add a new transaction.


3.4 Removing a Transaction

We already had this in Lab 3, but here’s the method again:

  void _deleteTransaction(String id) {
    setState(() {
      _transactions.removeWhere((tx) => tx.id == id);
    });
  }

Note: removeWhere() is a built-in method in Dart that removes all elements from a list that match a condition.


4. Creating a Simple Add Transaction Form

We’ll make a popup form when the user taps the Floating Action Button.


4.1 The Form Widget

Add this method inside _FinanceDashboardState:

  void _startAddTransaction(BuildContext context) {
    String title = '';
    String category = '';
    String amountStr = '';
    bool isExpense = true;

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (_) {
        return Padding(
          padding: EdgeInsets.only(
            bottom: MediaQuery.of(context).viewInsets.bottom,
            left: 16,
            right: 16,
            top: 16,
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                decoration: const InputDecoration(labelText: 'Title'),
                onChanged: (value) => title = value,
              ),
              TextField(
                decoration: const InputDecoration(labelText: 'Amount'),
                keyboardType: TextInputType.number,
                onChanged: (value) => amountStr = value,
              ),
              TextField(
                decoration: const InputDecoration(labelText: 'Category'),
                onChanged: (value) => category = value,
              ),
              SwitchListTile(
                title: const Text('Is Expense?'),
                value: isExpense,
                onChanged: (value) {
                  setState(() {
                    isExpense = value;
                  });
                },
              ),
              ElevatedButton(
                onPressed: () {
                  if (title.isEmpty || amountStr.isEmpty || category.isEmpty) {
                    return;
                  }
                  _addTransaction(
                    title,
                    double.parse(amountStr),
                    category,
                    isExpense,
                  );
                  Navigator.of(context).pop();
                },
                child: const Text('Add Transaction'),
              ),
            ],
          ),
        );
      },
    );
  }

4.2 Connecting the Floating Action Button

In DashboardScreen, update the FAB:

floatingActionButton: FloatingActionButton(
  onPressed: () => _startAddTransaction(context),
  tooltip: 'Add Transaction',
  child: const Icon(Icons.add),
),

But since the FAB is in DashboardScreen and _startAddTransaction is in FinanceDashboard,
we can move the FAB into FinanceDashboard for simplicity in this lab.


5. Final FinanceDashboard with State

Here’s the simplified version:

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

  @override
  State<FinanceDashboard> createState() => _FinanceDashboardState();
}

class _FinanceDashboardState extends State<FinanceDashboard> {
  final List<Transaction> _transactions = [];

  void _addTransaction(String title, double amount, String category, bool isExpense) {
    final newTx = Transaction(
      id: DateTime.now().toString(),
      title: title,
      amount: amount,
      date: DateTime.now(),
      category: category,
      isExpense: isExpense,
    );

    setState(() {
      _transactions.add(newTx);
    });
  }

  void _deleteTransaction(String id) {
    setState(() {
      _transactions.removeWhere((tx) => tx.id == id);
    });
  }

  void _startAddTransaction(BuildContext context) {
    String title = '';
    String category = '';
    String amountStr = '';
    bool isExpense = true;

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (_) {
        return Padding(
          padding: EdgeInsets.only(
            bottom: MediaQuery.of(context).viewInsets.bottom,
            left: 16,
            right: 16,
            top: 16,
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                decoration: const InputDecoration(labelText: 'Title'),
                onChanged: (value) => title = value,
              ),
              TextField(
                decoration: const InputDecoration(labelText: 'Amount'),
                keyboardType: TextInputType.number,
                onChanged: (value) => amountStr = value,
              ),
              TextField(
                decoration: const InputDecoration(labelText: 'Category'),
                onChanged: (value) => category = value,
              ),
              SwitchListTile(
                title: const Text('Is Expense?'),
                value: isExpense,
                onChanged: (value) {
                  setState(() {
                    isExpense = value;
                  });
                },
              ),
              ElevatedButton(
                onPressed: () {
                  if (title.isEmpty || amountStr.isEmpty || category.isEmpty) {
                    return;
                  }
                  _addTransaction(
                    title,
                    double.parse(amountStr),
                    category,
                    isExpense,
                  );
                  Navigator.of(context).pop();
                },
                child: const Text('Add Transaction'),
              ),
            ],
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const BalanceOverviewCard(),
            const SizedBox(height: 24),
            const Row(
              children: [
                Expanded(
                  child: SummaryCard(
                    title: 'Income',
                    amount: 0,
                    icon: Icons.arrow_upward,
                    color: Colors.green,
                  ),
                ),
                SizedBox(width: 16),
                Expanded(
                  child: SummaryCard(
                    title: 'Expenses',
                    amount: 0,
                    icon: Icons.arrow_downward,
                    color: Colors.red,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 24),
            const Text(
              'Recent Transactions',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            SizedBox(
              height: 400,
              child: TransactionList(
                transactions: _transactions,
                onDeleteTransaction: _deleteTransaction,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _startAddTransaction(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}

6. Key Takeaways

  • StatelessWidget → UI never changes after it’s built
  • StatefulWidget → UI can change when data changes
  • setState() → tells Flutter to rebuild the widget with new data
  • Keep setState() short — only update variables inside it
  • State is stored in the State class, not in the widget itself

7. Exercises

  1. Exercise 1: Add a validation message if the user enters a negative amount.
  2. Exercise 2: Update the Income and Expenses summary cards to calculate totals from _transactions.
  3. Exercise 3: Sort transactions so the newest appears first.

Lab 5: Forms and Input - Personal Finance Tracker

Objectives

By the end of this lab, students will:

  • Understand TextEditingController and how to manage form input
  • Learn form validation techniques in Flutter
  • Implement dropdown menus for category selection
  • Add date picker functionality
  • Create a complete transaction entry form with proper validation
  • Understand the difference between controlled and uncontrolled inputs

Prerequisites

  • Completed Lab 4 (basic state management)
  • Understanding of setState() and StatefulWidget
  • Familiarity with the Transaction model

1. Understanding Form Input in Flutter

In Flutter, there are two main ways to handle form input:

1.1 Using onChanged (Uncontrolled)

String title = '';

TextField(
  decoration: InputDecoration(labelText: 'Title'),
  onChanged: (value) => title = value,
)
  • The widget doesn't control the input value
  • We just listen to changes and store them in a variable

1.2 Using TextEditingController (Controlled)

final TextEditingController _titleController = TextEditingController();

TextField(
  controller: _titleController,
  decoration: InputDecoration(labelText: 'Title'),
)

// To get the value:
String title = _titleController.text;
  • The widget controls the input value
  • We can read, write, and clear the input programmatically
  • Better for validation and complex forms

2. TextEditingController Deep Dive

2.1 What is TextEditingController?

A TextEditingController is like a remote control for a text field. It allows you to:

  • Read the current text: controller.text
  • Set the text: controller.text = 'new value'
  • Clear the text: controller.clear()
  • Listen to changes: controller.addListener(() => print(controller.text))

2.2 Basic Usage

class MyForm extends StatefulWidget {
  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final TextEditingController _nameController = TextEditingController();

  @override
  void dispose() {
    // IMPORTANT: Always dispose controllers to prevent memory leaks
    _nameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _nameController,
          decoration: InputDecoration(labelText: 'Name'),
        ),
        ElevatedButton(
          onPressed: () {
            print('Name: ${_nameController.text}');
            _nameController.clear(); // Clear the field
          },
          child: Text('Submit'),
        ),
      ],
    );
  }
}

Important: Always call dispose() on controllers to prevent memory leaks!


3. Form Validation

3.1 Manual Validation

String? _validateTitle(String? value) {
  if (value == null || value.isEmpty) {
    return 'Title is required';
  }
  if (value.length < 3) {
    return 'Title must be at least 3 characters';
  }
  return null; // null means no error
}

3.2 Using Form Widget with GlobalKey

Flutter provides a Form widget that makes validation easier:

class MyForm extends StatefulWidget {
  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();
  final _titleController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField( // Note: TextFormField, not TextField
            controller: _titleController,
            decoration: InputDecoration(labelText: 'Title'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Title is required';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // Form is valid, proceed
                print('Title: ${_titleController.text}');
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

4. Input Widgets Overview

4.1 TextField vs TextFormField

  • TextField: Basic text input widget
  • TextFormField: TextField + built-in validation support

4.2 Common Input Types

// Text input
TextFormField(
  decoration: InputDecoration(labelText: 'Name'),
)

// Number input
TextFormField(
  keyboardType: TextInputType.number,
  decoration: InputDecoration(labelText: 'Amount'),
)

// Email input
TextFormField(
  keyboardType: TextInputType.emailAddress,
  decoration: InputDecoration(labelText: 'Email'),
)

// Password input
TextFormField(
  obscureText: true,
  decoration: InputDecoration(labelText: 'Password'),
)

// Multiline input
TextFormField(
  maxLines: 3,
  decoration: InputDecoration(labelText: 'Description'),
)

5. Implementing the Complete Transaction Form

Let's create a proper transaction form with validation. Create a new file lib/widgets/add_transaction_form.dart:

import 'package:flutter/material.dart';

class AddTransactionForm extends StatefulWidget {
  final Function(String, double, String, bool) onAddTransaction;

  const AddTransactionForm({super.key, required this.onAddTransaction});

  @override
  State<AddTransactionForm> createState() => _AddTransactionFormState();
}

class _AddTransactionFormState extends State<AddTransactionForm> {
  // Form key for validation
  final _formKey = GlobalKey<FormState>();

  // Controllers for text inputs
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();

  // Form state variables
  String _selectedCategory = 'Food';
  bool _isExpense = true;

  // Available categories
  final List<String> _categories = [
    'Food',
    'Shopping',
    'Transport',
    'Entertainment',
    'Utilities',
    'Salary',
    'Gift',
    'Other',
  ];

  @override
  void dispose() {
    // Clean up controllers
    _titleController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  // Method to submit the form
  void _submitForm() {
    if (!_formKey.currentState!.validate()) {
      return; // Form is not valid
    }

    final title = _titleController.text;
    final amount = double.parse(_amountController.text);

    widget.onAddTransaction(title, amount, _selectedCategory, _isExpense);

    Navigator.of(context).pop(); // Close the form
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(
        bottom: MediaQuery.of(context).viewInsets.bottom,
        left: 16,
        right: 16,
        top: 16,
      ),
      child: Form(
        key: _formKey,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Form title
            const Text(
              'Add New Transaction',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 20),

            // Title input
            TextFormField(
              controller: _titleController,
              decoration: const InputDecoration(
                labelText: 'Transaction Title',
                hintText: 'e.g., Grocery Shopping',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.title),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter a title';
                }
                if (value.length < 3) {
                  return 'Title must be at least 3 characters';
                }
                return null;
              },
            ),
            const SizedBox(height: 16),

            // Amount input
            TextFormField(
              controller: _amountController,
              decoration: const InputDecoration(
                labelText: 'Amount',
                hintText: '0.00',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.attach_money),
              ),
              keyboardType: TextInputType.numberWithOptions(decimal: true),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter an amount';
                }
                final amount = double.tryParse(value);
                if (amount == null) {
                  return 'Please enter a valid number';
                }
                if (amount <= 0) {
                  return 'Amount must be greater than 0';
                }
                return null;
              },
            ),
            const SizedBox(height: 16),

            // Category dropdown
            DropdownButtonFormField<String>(
              value: _selectedCategory,
              decoration: const InputDecoration(
                labelText: 'Category',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.category),
              ),
              items: _categories.map((category) {
                return DropdownMenuItem(value: category, child: Text(category));
              }).toList(),
              onChanged: (value) {
                setState(() {
                  _selectedCategory = value!;
                });
              },
            ),
            const SizedBox(height: 16),

            // Income/Expense toggle
            Card(
              child: SwitchListTile(
                title: Text(_isExpense ? 'Expense' : 'Income'),
                subtitle: Text(
                  _isExpense ? 'Money going out' : 'Money coming in',
                ),
                value: _isExpense,
                onChanged: (value) {
                  setState(() {
                    _isExpense = value;
                  });
                },
                secondary: Icon(
                  _isExpense ? Icons.arrow_downward : Icons.arrow_upward,
                  color: _isExpense ? Colors.red : Colors.green,
                ),
              ),
            ),
            const SizedBox(height: 24),

            // Submit button
            ElevatedButton(
              onPressed: _submitForm,
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: const Text(
                'Add Transaction',
                style: TextStyle(fontSize: 16),
              ),
            ),
            const SizedBox(height: 16),
          ],
        ),
      ),
    );
  }
}

6. Updating the Dashboard to Use the New Form

Now let's update the FinanceDashboard to use our new form, update the _FinanceDashboardState class:

class _FinanceDashboardState extends State<FinanceDashboard> {
  String? _selectedCategory;

  // Add this method to the class
  List<Transaction> get _filteredTransactions {
    var filtered = _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;
  }

  String _searchQuery = '';

  // Sample transaction data
  final List<Transaction> _transactions = [
    Transaction(
      id: 't1',
      title: 'Grocery Shopping',
      amount: 45.99,
      date: DateTime.now().subtract(const Duration(days: 1)),
      category: 'Food',
      isExpense: true,
    ),
    Transaction(
      id: 't2',
      title: 'Monthly Salary',
      amount: 1500.00,
      date: DateTime.now().subtract(const Duration(days: 3)),
      category: 'Salary',
      isExpense: false,
    ),
    Transaction(
      id: 't3',
      title: 'New Headphones',
      amount: 99.99,
      date: DateTime.now().subtract(const Duration(days: 5)),
      category: 'Shopping',
      isExpense: true,
    ),
    Transaction(
      id: 't4',
      title: 'Restaurant Dinner',
      amount: 35.50,
      date: DateTime.now().subtract(const Duration(days: 7)),
      category: 'Food',
      isExpense: true,
    ),
  ];

  void _deleteTransaction(String id) {
    setState(() {
      _transactions.removeWhere((tx) => tx.id == id);
    });
  }

  void _addTransaction(
    String title,
    double amount,
    String category,
    bool isExpense,
  ) {
    final newTx = Transaction(
      id: DateTime.now().toString(),
      title: title,
      amount: amount,
      date: DateTime.now(),
      category: category,
      isExpense: isExpense,
    );

    setState(() {
      _transactions.add(newTx);
    });
  }

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

  // Calculate totals for summary cards
  double get _totalIncome {
    return _transactions
        .where((tx) => !tx.isExpense)
        .fold(0.0, (sum, tx) => sum + tx.amount);
  }

  double get _totalExpenses {
    return _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(
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Header section
            const Text(
              'My Balance',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),

            // Balance Card
            BalanceOverviewCard(balance: _balance),
            const SizedBox(height: 24),

            // Income & Expenses Row
            const Text(
              'Summary',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),

            // Income and Expense cards
            Row(
              children: [
                Expanded(
                  child: SummaryCard(
                    title: 'Income',
                    amount: _totalIncome,
                    icon: Icons.arrow_upward,
                    color: Colors.green,
                  ),
                ),
                SizedBox(width: 16),
                Expanded(
                  child: SummaryCard(
                    title: 'Expenses',
                    amount: _totalExpenses,
                    icon: Icons.arrow_downward,
                    color: Colors.red,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 24),
            Padding(
              padding: const EdgeInsets.only(bottom: 16),
              child: TextField(
                decoration: InputDecoration(
                  hintText: 'Search transactions...',
                  prefixIcon: const Icon(Icons.search),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                  contentPadding: const EdgeInsets.symmetric(
                    vertical: 0,
                    horizontal: 16,
                  ),
                ),
                onChanged: (value) {
                  setState(() {
                    _searchQuery = value;
                  });
                },
              ),
            ),
            // Recent Transactions Header
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  'Recent Transactions',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                DropdownButton<String?>(
                  hint: const Text('All Categories'),
                  value: _selectedCategory,
                  onChanged: (newValue) {
                    setState(() {
                      _selectedCategory = newValue;
                    });
                  },
                  items: [
                    const DropdownMenuItem<String?>(
                      value: null,
                      child: Text('All Categories'),
                    ),
                    ...{..._transactions.map((tx) => tx.category)}.map(
                      (category) => DropdownMenuItem<String>(
                        value: category,
                        child: Text(category),
                      ),
                    ),
                  ],
                ),
              ],
            ),
            const SizedBox(height: 16),

            // Transaction List
            SizedBox(
              height: 400, // Fixed height for the list
              child: GroupedTransactionList(
                transactions: _filteredTransactions,
                onDeleteTransaction: _deleteTransaction,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _startAddTransaction(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}

7. Updating the Balance Overview Card

Update the BalanceOverviewCard to accept dynamic balance data. In your existing card widget:

class BalanceOverviewCard extends StatelessWidget {
  const BalanceOverviewCard({super.key, required this.balance});
  final double 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: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  'Current Balance',
                  style: TextStyle(fontSize: 16, color: Colors.grey),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 10,
                    vertical: 5,
                  ),
                  decoration: BoxDecoration(
                    color: Colors.green.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: const Row(
                    children: [
                      Icon(Icons.arrow_upward, size: 14, color: Colors.green),
                      SizedBox(width: 4),
                      Text(
                        '2.5%',
                        style: TextStyle(
                          color: Colors.green,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            const Text(
              '\$400.00',
              style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
          ],
        ),
      ),
    );
  }
}

8. Form Validation Best Practices

8.1 Common Validation Rules

// Required field
String? validateRequired(String? value) {
  if (value == null || value.isEmpty) {
    return 'This field is required';
  }
  return null;
}

// Email validation
String? validateEmail(String? value) {
  if (value == null || value.isEmpty) {
    return 'Email is required';
  }
  if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
    return 'Please enter a valid email';
  }
  return null;
}

// Number validation
String? validateAmount(String? value) {
  if (value == null || value.isEmpty) {
    return 'Amount is required';
  }
  final amount = double.tryParse(value);
  if (amount == null) {
    return 'Please enter a valid number';
  }
  if (amount <= 0) {
    return 'Amount must be greater than 0';
  }
  return null;
}

8.2 Real-time Validation

You can validate as the user types:

TextFormField(
  controller: _titleController,
  decoration: InputDecoration(labelText: 'Title'),
  validator: validateRequired,
  autovalidateMode: AutovalidateMode.onUserInteraction,
)

9. Exercises

Exercise 1: Add Amount Formatting

Format the amount input to show currency as the user types:

// Add this package: flutter pub add intl

TextFormField(
  controller: _amountController,
  decoration: const InputDecoration(
    labelText: 'Amount',
    prefixText: '\$ ',
    border: OutlineInputBorder(),
  ),
  keyboardType: TextInputType.numberWithOptions(decimal: true),
  onChanged: (value) {
    // Format the input as currency
    if (value.isNotEmpty) {
      final amount = double.tryParse(value);
      if (amount != null) {
        final formatted = NumberFormat.currency(symbol: '').format(amount);
        // Update controller without triggering onChanged again
      }
    }
  },
)

Exercise 2: Add Form Reset

Add a "Clear Form" button that resets all fields:

void _clearForm() {
  _titleController.clear();
  _amountController.clear();
  setState(() {
    _selectedCategory = 'Food';
    _isExpense = true;
  });
}

// Add this button next to the submit button
OutlinedButton(
  onPressed: _clearForm,
  child: const Text('Clear Form'),
)

10. Common Issues and Troubleshooting

10.1 Controller Memory Leaks

Problem: Not disposing controllers Solution: Always call dispose() in the State class

10.2 Form Not Validating

Problem: Using TextField instead of TextFormField Solution: Use TextFormField with a Form widget

10.3 Keyboard Covering Input

Problem: Bottom sheet doesn't adjust for keyboard Solution: Use MediaQuery.of(context).viewInsets.bottom in padding

10.4 Date Picker Not Updating

Problem: Forgetting to call setState() after date selection Solution: Always wrap state changes in setState()


11. Key Takeaways

  • TextEditingController gives you full control over text inputs
  • Form validation prevents bad data from entering your app
  • TextFormField + Form widget = easy validation
  • DropdownButtonFormField for selecting from predefined options
  • Date pickers require showDatePicker() and state management
  • Always dispose controllers to prevent memory leaks
  • Real-time validation improves user experience

Next Steps

In the next lab, we'll implement navigation between different screens, create a transaction detail view, and add a bottom navigation bar to organize our app into multiple sections.

Lab 6: Navigation - Personal Finance Tracker

Objectives

By the end of this lab, students will:

  • Understand different types of navigation in Flutter
  • Learn how to navigate between screens using Navigator
  • Implement bottom navigation bar for app sections
  • Create a transaction detail screen
  • Understand the navigation stack concept
  • Learn how to pass data between screens

Prerequisites

  • Completed Lab 5 (forms and input)
  • Understanding of StatefulWidget and setState()
  • Familiarity with the Transaction model and form implementation

1. Understanding Navigation in Flutter

1.1 What is Navigation?

Navigation is how users move between different screens (pages) in your app. Think of it like turning pages in a book or switching between tabs in a web browser.

1.2 The Navigation Stack

Flutter uses a stack-based navigation system. Imagine a stack of cards:

  • Push: Add a new screen on top of the stack
  • Pop: Remove the current screen and go back to the previous one
┌─────────────────┐
│   Detail Screen │ ← Current screen (top of stack)
├─────────────────┤
│   Home Screen   │ ← Previous screen
├─────────────────┤
│   Login Screen  │ ← Bottom of stack
└─────────────────┘

1.3 Types of Navigation

1. Push Navigation (Forward)

Moving to a new screen:

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => NewScreen()),
);

2. Pop Navigation (Back)

Going back to the previous screen:

Navigator.pop(context);

3. Replace Navigation

Replace current screen with a new one:

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => NewScreen()),
);

4. Named Routes

Navigate using route names (like URLs):

Navigator.pushNamed(context, '/details');

2. Basic Navigation Example

Let's start with a simple example:

// Screen 1
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => DetailScreen()),
            );
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

// Screen 2
class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context); // Go back
          },
          child: Text('Go Back'),
        ),
      ),
    );
  }
}

3. Passing Data Between Screens

3.1 Passing Data Forward (Push)

// From Screen A to Screen B
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailScreen(data: 'Hello World'),
  ),
);

// Screen B receives the data
class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key});
  final String data;

  const DetailScreen({super.key, required this.data});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('Received: $data'),
    );
  }
}

3.2 Passing Data Back (Pop)

// Screen B sends data back
Navigator.pop(context, 'Result from Screen B');

// Screen A receives the data
final result = await Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => DetailScreen()),
);
print('Received back: $result');

4. Bottom Navigation Bar

4.1 What is Bottom Navigation Bar?

A bottom navigation bar allows users to switch between different sections of your app. It's like having multiple "tabs" at the bottom of the screen.

4.2 Basic Implementation

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

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

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;

  final List<Widget> _screens = [HomeScreen(), StatsScreen(), SettingsScreen()];

  @override
  Widget build(BuildContext context) {
    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: 'Stats'),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text('StatsScreen')));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text('HomeScreen')));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text('SettingsScreen')));
  }
}

5. Implementing Navigation in Our Finance Tracker

5.1 Create the Transaction Detail Screen

First, let's create a screen to show transaction details. Create a new file lib/screens/transaction_detail_screen.dart:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/transaction.dart';

class TransactionDetailScreen extends StatelessWidget {
  final Transaction transaction;

  const TransactionDetailScreen({super.key, required this.transaction});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Transaction Details'),
        backgroundColor: Theme.of(context).colorScheme.primary,
        foregroundColor: Theme.of(context).colorScheme.onPrimary,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Transaction Type Badge
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(
                color: transaction.isExpense
                    ? Colors.red.withOpacity(0.1)
                    : Colors.green.withOpacity(0.1),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                transaction.isExpense ? 'EXPENSE' : 'INCOME',
                style: TextStyle(
                  color: transaction.isExpense ? Colors.red : Colors.green,
                  fontWeight: FontWeight.bold,
                  fontSize: 12,
                ),
              ),
            ),
            const SizedBox(height: 24),

            // Amount
            Text(
              '${transaction.isExpense ? '-' : '+'}\$${transaction.amount.toStringAsFixed(2)}',
              style: TextStyle(
                fontSize: 36,
                fontWeight: FontWeight.bold,
                color: transaction.isExpense ? Colors.red : Colors.green,
              ),
            ),
            const SizedBox(height: 32),

            // Details Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    _buildDetailRow(
                      Icons.title,
                      'Title',
                      transaction.title,
                    ),
                    const Divider(),
                    _buildDetailRow(
                      Icons.category,
                      'Category',
                      transaction.category,
                    ),
                    const Divider(),
                    _buildDetailRow(
                      Icons.calendar_today,
                      'Date',
                      DateFormat.yMMMMd().format(transaction.date),
                    ),
                    const Divider(),
                    _buildDetailRow(
                      Icons.access_time,
                      'Time',
                      DateFormat.jm().format(transaction.date),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 32),

            // Action Buttons
            Row(
              children: [
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () {
                      // TODO: Implement edit functionality
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(
                          content: Text('Edit feature coming soon!'),
                        ),
                      );
                    },
                    icon: const Icon(Icons.edit),
                    label: const Text('Edit'),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: () {
                      _showDeleteDialog(context);
                    },
                    icon: const Icon(Icons.delete),
                    label: const Text('Delete'),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.red,
                      foregroundColor: Colors.white,
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDetailRow(IconData icon, String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Row(
        children: [
          Icon(icon, color: Colors.grey),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  label,
                  style: const TextStyle(
                    fontSize: 12,
                    color: Colors.grey,
                  ),
                ),
                Text(
                  value,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w500,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  void _showDeleteDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Delete Transaction'),
          content: const Text(
            'Are you sure you want to delete this transaction? This action cannot be undone.',
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(); // Close dialog
              },
              child: const Text('Cancel'),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop(); // Close dialog
                Navigator.of(context).pop(transaction.id); // Return to previous screen with delete signal
              },
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.red,
                foregroundColor: Colors.white,
              ),
              child: const Text('Delete'),
            ),
          ],
        );
      },
    );
  }
}

5.2 Create a Statistics Screen

Create a new file lib/screens/statistics_screen.dart:

import 'package:flutter/material.dart';
import '../models/transaction.dart';

class StatisticsScreen extends StatelessWidget {
  final List<Transaction> transactions;

  const StatisticsScreen({super.key, required this.transactions});

  @override
  Widget build(BuildContext context) {
    final totalIncome = transactions
        .where((tx) => !tx.isExpense)
        .fold(0.0, (sum, tx) => sum + tx.amount);

    final totalExpenses = transactions
        .where((tx) => tx.isExpense)
        .fold(0.0, (sum, tx) => sum + tx.amount);

    final balance = totalIncome - totalExpenses;

    // Group expenses by category
    final Map<String, double> expensesByCategory = {};
    for (var tx in transactions.where((tx) => tx.isExpense)) {
      expensesByCategory[tx.category] =
          (expensesByCategory[tx.category] ?? 0) + tx.amount;
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Statistics'),
        backgroundColor: Theme.of(context).colorScheme.primary,
        foregroundColor: Theme.of(context).colorScheme.onPrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Overview Cards
            Row(
              children: [
                Expanded(
                  child: _buildStatCard(
                    'Total Income',
                    '\$${totalIncome.toStringAsFixed(2)}',
                    Colors.green,
                    Icons.arrow_upward,
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: _buildStatCard(
                    'Total Expenses',
                    '\$${totalExpenses.toStringAsFixed(2)}',
                    Colors.red,
                    Icons.arrow_downward,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),

            _buildStatCard(
              'Net Balance',
              '\$${balance.toStringAsFixed(2)}',
              balance >= 0 ? Colors.green : Colors.red,
              balance >= 0 ? Icons.trending_up : Icons.trending_down,
            ),
            const SizedBox(height: 32),

            // Expenses by Category
            const Text(
              'Expenses by Category',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),

            if (expensesByCategory.isEmpty)
              const Center(
                child: Text(
                  'No expenses to show',
                  style: TextStyle(color: Colors.grey),
                ),
              )
            else
              ...expensesByCategory.entries.map((entry) {
                final percentage = (entry.value / totalExpenses * 100);
                return _buildCategoryRow(
                  entry.key,
                  entry.value,
                  percentage,
                );
              }).toList(),
          ],
        ),
      ),
    );
  }

  Widget _buildStatCard(String title, String value, Color color, IconData icon) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(icon, color: color),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    title,
                    style: const TextStyle(
                      fontSize: 14,
                      color: Colors.grey,
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              value,
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: color,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildCategoryRow(String category, double amount, double percentage) {
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  category,
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
                Text(
                  '\$${amount.toStringAsFixed(2)}',
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: LinearProgressIndicator(
                    value: percentage / 100,
                    backgroundColor: Colors.grey.shade200,
                  ),
                ),
                const SizedBox(width: 8),
                Text('${percentage.toStringAsFixed(1)}%'),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

5.3 Create a Settings Screen

Create a new file lib/screens/settings_screen.dart:

import 'package:flutter/material.dart';

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

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

class _SettingsScreenState extends State<SettingsScreen> {
  bool _darkMode = false;
  bool _notifications = true;
  String _currency = 'USD';

  @override
  Widget build(BuildContext context) {
    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: (value) {
                    setState(() {
                      _darkMode = value;
                    });
                  },
                  secondary: const Icon(Icons.dark_mode),
                ),
                const Divider(height: 1),
                SwitchListTile(
                  title: const Text('Notifications'),
                  subtitle: const Text('Receive transaction alerts'),
                  value: _notifications,
                  onChanged: (value) {
                    setState(() {
                      _notifications = value;
                    });
                  },
                  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('Backup & Sync'),
                  subtitle: const Text('Sync data across devices'),
                  leading: const Icon(Icons.cloud_sync),
                  trailing: const Icon(Icons.arrow_forward_ios),
                  onTap: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Backup 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();
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  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) {
                  setState(() {
                    _currency = value!;
                  });
                  Navigator.of(context).pop();
                },
              );
            }).toList(),
          ),
        );
      },
    );
  }

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

5.4 Update the Main App Structure

Now let's update our main app to use bottom navigation. Update your main.dart:

import 'package:flutter/material.dart';
import 'package:personal_finance_tracker/dashboard_screen.dart';
import 'models/transaction.dart';
import 'screens/statistics_screen.dart';
import 'screens/settings_screen.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;

  // Sample transaction data
  final List<Transaction> _transactions = [
    Transaction(
      id: 't1',
      title: 'Grocery Shopping',
      amount: 45.99,
      date: DateTime.now().subtract(const Duration(days: 1)),
      category: 'Food',
      isExpense: true,
    ),
    Transaction(
      id: 't2',
      title: 'Monthly Salary',
      amount: 1500.00,
      date: DateTime.now().subtract(const Duration(days: 3)),
      category: 'Salary',
      isExpense: false,
    ),
    Transaction(
      id: 't3',
      title: 'New Headphones',
      amount: 99.99,
      date: DateTime.now().subtract(const Duration(days: 5)),
      category: 'Shopping',
      isExpense: true,
    ),
  ];

  void _addTransaction(
    String title,
    double amount,
    String category,
    bool isExpense,
  ) {
    final newTx = Transaction(
      id: DateTime.now().toString(),
      title: title,
      amount: amount,
      date: DateTime.now(),
      category: category,
      isExpense: isExpense,
    );

    setState(() {
      _transactions.add(newTx);
    });
  }

  void _deleteTransaction(String id) {
    setState(() {
      _transactions.removeWhere((tx) => tx.id == id);
    });
  }

  @override
  Widget build(BuildContext context) {
    final List<Widget> screens = [
      DashboardScreen(
        transactions: _transactions,
        onAddTransaction: _addTransaction,
        onDeleteTransaction: _deleteTransaction,
      ),
      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',
          ),
        ],
      ),
    );
  }
}

and update the DashboardScreen in lib/dashboard_screen.dart:

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

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

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

  @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),
      ),
      body: SingleChildScrollView(
        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),
      ),
    );
  }
}

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

5.5 Update the Transaction List Widget

Update your TransactionList widget to handle taps. In lib/widgets/transaction_list.dart, modify the TransactionList class:

class TransactionList extends StatelessWidget {
  final List<Transaction> transactions;
  final Function(String) onDeleteTransaction;
  final Function(Transaction)? onTransactionTap;

  const TransactionList({
    super.key,
    required this.transactions,
    required this.onDeleteTransaction,
    this.onTransactionTap,
  });

  @override
  Widget build(BuildContext context) {
    if (transactions.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.receipt_long,
              size: 64,
              color: Colors.grey.shade400,
            ),
            const SizedBox(height: 16),
            Text(
              'No transactions yet!',
              style: TextStyle(
                fontSize: 18,
                color: Colors.grey.shade600,
              ),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      itemCount: transactions.length,
      itemBuilder: (ctx, index) {
        return TransactionCard(
          transaction: transactions[index],
          onDelete: onDeleteTransaction,
          onTap: onTransactionTap,
        );
      },
    );
  }
}

And update the TransactionCard class:

class TransactionCard extends StatelessWidget {
  final Transaction transaction;
  final Function(String) onDelete;
  final Function(Transaction)? onTap;

  const TransactionCard({
    super.key,
    required this.transaction,
    required this.onDelete,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
      elevation: 2,
      child: ListTile(
        onTap: onTap != null ? () => onTap!(transaction) : null,
        leading: CircleAvatar(
          radius: 24,
          backgroundColor: transaction.isExpense
              ? Colors.red.shade100
              : Colors.green.shade100,
          child: Icon(
            getCategoryIcon(transaction.category),
            color: transaction.isExpense ? Colors.red : Colors.green,
          ),
        ),
        title: Text(
          transaction.title,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
        subtitle: Text(
          '${transaction.category} • ${DateFormat.yMMMd().format(transaction.date)}',
        ),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              '${transaction.isExpense ? '-' : '+'}\$${transaction.amount.toStringAsFixed(2)}',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color: transaction.isExpense ? Colors.red : Colors.green,
              ),
            ),
            IconButton(
              icon: const Icon(Icons.delete_outline, color: Colors.grey),
              onPressed: () => onDelete(transaction.id),
            ),
          ],
        ),
      ),
    );
  }

  IconData getCategoryIcon(String category) {
    switch (category.toLowerCase()) {
      case 'food':
        return Icons.restaurant;
      case 'shopping':
        return Icons.shopping_bag;
      case 'transport':
        return Icons.directions_car;
      case 'entertainment':
        return Icons.movie;
      case 'utilities':
        return Icons.receipt;
      case 'salary':
        return Icons.work;
      case 'gift':
        return Icons.card_giftcard;
      default:
        return Icons.attach_money;
    }
  }
}

6. Key Navigation Concepts

6.1 Navigation Stack

  • Push: Add new screen on top
  • Pop: Remove current screen and go back
  • Replace: Replace current screen with new one

6.2 Passing Data

  • Forward: Pass data when navigating to a new screen
  • Backward: Return data when going back

6.3 Bottom Navigation

  • Switches between different sections of the app
  • Maintains separate navigation stacks for each tab
  • Uses setState() to change the current index

7. Exercises

Exercise 1: Add Navigation Animation

Customize the navigation animation:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => TransactionDetailScreen(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return SlideTransition(
        position: animation.drive(
          Tween(begin: const Offset(1.0, 0.0), end: Offset.zero),
        ),
        child: child,
      );
    },
  ),
);

Exercise 2: Add a Search Screen

Create a dedicated search screen accessible from the app bar:

// Add to AppBar actions
actions: [
  IconButton(
    icon: const Icon(Icons.search),
    onPressed: () {
      Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SearchScreen()),
      );
    },
  ),
],

Exercise 3: Implement Edit Transaction

Add edit functionality to the transaction detail screen that navigates to an edit form.


8. Common Issues and Troubleshooting

8.1 Navigator Context Issues

Problem: Navigator.of(context) returns null Solution: Make sure you're using the correct BuildContext

8.2 Bottom Navigation Not Updating

Problem: Tapping bottom nav items doesn't change screens Solution: Make sure you're calling setState() when changing _currentIndex

8.3 Data Not Passing Between Screens

Problem: Data is null in the destination screen Solution: Check that you're passing data correctly in the constructor

8.4 Back Button Not Working

Problem: Custom back button doesn't work Solution: Use Navigator.pop(context) or Navigator.of(context).pop()


9. Key Takeaways

  • Navigation is how users move between screens in your app
  • Navigator.push() goes forward, Navigator.pop() goes back
  • Bottom Navigation Bar organizes your app into sections
  • Pass data between screens using constructors and return values
  • setState() is needed to update the UI when navigation state changes
  • Always handle null cases when receiving data from navigation

Next Steps

In the next lab, we'll implement local storage using SQLite to persist our transaction data. We'll learn how to save, retrieve, and manage data locally on the device so that transactions don't disappear when the app is closed.

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.

Lab 8: RESTful APIs - Personal Finance Tracker

Objectives

By the end of this lab, students will:

  • Understand what RESTful APIs are and how they work
  • Learn how to make HTTP requests in Flutter
  • Work with JSON data from external APIs
  • Implement currency conversion using a real API
  • Handle loading states and network errors
  • Add offline fallback functionality
  • Integrate API features into the finance tracker

Prerequisites

  • Completed Lab 7 (local storage implementation)
  • Understanding of async/await and Future
  • Familiarity with the finance tracker structure
  • Basic understanding of JSON format

1. Understanding RESTful APIs

1.1 What is an API?

API stands for Application Programming Interface. It's like a waiter in a restaurant:

  • You (the app) are the customer
  • The API is the waiter
  • The server (external service) is the kitchen

You tell the waiter (API) what you want, the waiter tells the kitchen (server), and the kitchen sends back your food (data).

1.2 What is REST?

REST stands for Representational State Transfer. It's a set of rules for how APIs should work:

  • GET: Ask for data (like reading a book)
  • POST: Send new data (like writing a new page)
  • PUT: Update existing data (like editing a page)
  • DELETE: Remove data (like tearing out a page)

1.3 What is JSON?

JSON (JavaScript Object Notation) is the language APIs use to send data. It looks like this:

{
  "name": "John",
  "age": 25,
  "city": "New York",
  "hobbies": ["reading", "swimming"]
}

2. Understanding HTTP Requests

2.1 How HTTP Requests Work

When your app wants data from the internet:

  1. App sends request: "Can I have the current USD to EUR exchange rate?"
  2. Server processes: The server looks up the exchange rate
  3. Server sends response: "1 USD = 0.92 EUR"
  4. App uses the data: Display the converted amount

2.2 Parts of an HTTP Request

GET https://api.frankfurter.dev/v1/latest?from=USD&to=EUR
│   │                                │
│   └─ URL (where to send request)   └─ Parameters (what you want)
└─ Method (what you want to do)

2.3 HTTP Response

{
  "amount": 1.0,
  "base": "USD",
  "date": "2024-12-09",
  "rates": {
    "EUR": 0.92
  }
}

3. Setting Up HTTP Requests in Flutter

3.1 Add HTTP Package

Add the HTTP package 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
  http: ^1.5.0 # Add this line

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

Run the command to install:

flutter pub get

or run this command to add and install the packages:

flutter pub add http

3.2 Import the HTTP Package

In your Dart files, import the package:

import 'dart:convert';
import 'package:http/http.dart' as http;

4. Basic HTTP Request Example

4.1 Simple GET Request

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> fetchData() async {
  // 1. Create the URL
  final url = Uri.parse('https://api.frankfurter.dev/v1/latest?from=USD&to=EUR');

  try {
    // 2. Make the request
    final response = await http.get(url);

    // 3. Check if the request was successful
    if (response.statusCode == 200) {
      // 4. Convert JSON string to Dart object
      final data = json.decode(response.body);
      print('Exchange rate: ${data['rates']['EUR']}');
    } else {
      print('Error: ${response.statusCode}');
    }
  } catch (e) {
    print('Network error: $e');
  }
}

4.2 Understanding the Code

  • Uri.parse(): Converts a string URL into a Uri object
  • http.get(): Makes a GET request to the URL
  • response.statusCode: HTTP status (200 = success, 404 = not found, etc.)
  • json.decode(): Converts JSON string to Dart Map/List
  • try-catch: Handles network errors (no internet, server down, etc.)

5. Creating a Currency Conversion Service

5.1 Create the Currency Model

Create a new file lib/models/currency_rate.dart:

class CurrencyRate {
  final double amount;
  final String baseCurrency;
  final String targetCurrency;
  final double rate;
  final DateTime date;

  const CurrencyRate({
    required this.amount,
    required this.baseCurrency,
    required this.targetCurrency,
    required this.rate,
    required this.date,
  });

  factory CurrencyRate.fromJson(Map<String, dynamic> json, String targetCurrency) {
    return CurrencyRate(
      amount: json['amount'].toDouble(),
      baseCurrency: json['base'],
      targetCurrency: targetCurrency,
      rate: json['rates'][targetCurrency].toDouble(),
      date: DateTime.parse(json['date']),
    );
  }

  double get convertedAmount => amount * rate;

  @override
  String toString() {
    return '$amount $baseCurrency = ${convertedAmount.toStringAsFixed(2)} $targetCurrency';
  }
}

5.2 Create the Currency Service

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

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/currency_rate.dart';

class CurrencyService {
  static const String _baseUrl = 'https://api.frankfurter.dev/v1';
  static const Duration _timeout = Duration(seconds: 10);

  // Get supported currencies
  Future<List<String>> getSupportedCurrencies() async {
    try {
      final url = Uri.parse('$_baseUrl/currencies');
      final response = await http.get(url).timeout(_timeout);

      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);
        return data.keys.toList()..sort();
      } else {
        throw Exception('Failed to load currencies: ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error fetching currencies: $e');
      // Return default currencies if API fails
      return ['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD'];
    }
  }

  // Convert currency
  Future<CurrencyRate> convertCurrency({
    required double amount,
    required String fromCurrency,
    required String toCurrency,
  }) async {
    // Return same currency if from and to are the same
    if (fromCurrency == toCurrency) {
      return CurrencyRate(
        amount: amount,
        baseCurrency: fromCurrency,
        targetCurrency: toCurrency,
        rate: 1.0,
        date: DateTime.now(),
      );
    }

    try {
      final url = Uri.parse(
        '$_baseUrl/latest?amount=$amount&from=$fromCurrency&to=$toCurrency',
      );

      final response = await http.get(url).timeout(_timeout);

      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);
        return CurrencyRate.fromJson(data, toCurrency);
      } else {
        throw Exception('Failed to convert currency: ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error converting currency: $e');
      rethrow; // Re-throw the error so the UI can handle it
    }
  }
}

5.3 Understanding the Currency Service

Key Features:

  • getSupportedCurrencies(): Gets list of available currencies
  • convertCurrency(): Converts amount from one currency to another
  • Error handling: Returns default data if API fails
  • Timeout: Prevents app from hanging if network is slow

6. Creating the Currency Conversion UI

6.1 Create the Currency Conversion Screen

Create a new file lib/screens/currency_converter_screen.dart:

import 'package:flutter/material.dart';
import '../models/currency_rate.dart';
import '../services/currency_service.dart';

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

  @override
  State<CurrencyConverterScreen> createState() => _CurrencyConverterScreenState();
}

class _CurrencyConverterScreenState extends State<CurrencyConverterScreen> {
  final CurrencyService _currencyService = CurrencyService();
  final TextEditingController _amountController = TextEditingController(text: '1.0');

  List<String> _currencies = [];
  String _fromCurrency = 'USD';
  String _toCurrency = 'EUR';
  CurrencyRate? _currentRate;
  bool _isLoading = false;
  bool _isLoadingCurrencies = true;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    _loadCurrencies();
    _convertCurrency();
  }

  @override
  void dispose() {
    _amountController.dispose();
    super.dispose();
  }

  Future<void> _loadCurrencies() async {
    try {
      final currencies = await _currencyService.getSupportedCurrencies();
      setState(() {
        _currencies = currencies;
        _isLoadingCurrencies = false;
      });
    } catch (e) {
      setState(() {
        _isLoadingCurrencies = false;
        _errorMessage = 'Failed to load currencies';
      });
    }
  }

  Future<void> _convertCurrency() async {
    final amountText = _amountController.text.trim();
    if (amountText.isEmpty) return;

    final amount = double.tryParse(amountText);
    if (amount == null || amount <= 0) {
      setState(() {
        _errorMessage = 'Please enter a valid amount';
        _currentRate = null;
      });
      return;
    }

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

    try {
      final rate = await _currencyService.convertCurrency(
        amount: amount,
        fromCurrency: _fromCurrency,
        toCurrency: _toCurrency,
      );

      setState(() {
        _currentRate = rate;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
        _errorMessage = 'Failed to convert currency. Please check your internet connection.';
        _currentRate = null;
      });
    }
  }

  void _swapCurrencies() {
    setState(() {
      final temp = _fromCurrency;
      _fromCurrency = _toCurrency;
      _toCurrency = temp;
    });
    _convertCurrency();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Currency Converter'),
        backgroundColor: Theme.of(context).colorScheme.primary,
        foregroundColor: Theme.of(context).colorScheme.onPrimary,
      ),
      body: _isLoadingCurrencies
          ? const Center(child: CircularProgressIndicator())
          : SingleChildScrollView(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Amount Input
                  Card(
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            'Amount',
                            style: TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          const SizedBox(height: 8),
                          TextField(
                            controller: _amountController,
                            keyboardType: const TextInputType.numberWithOptions(decimal: true),
                            decoration: InputDecoration(
                              hintText: 'Enter amount',
                              border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(12),
                              ),
                              prefixIcon: const Icon(Icons.attach_money),
                            ),
                            onChanged: (value) {
                              // Auto-convert after user stops typing
                              Future.delayed(const Duration(milliseconds: 500), () {
                                if (_amountController.text == value) {
                                  _convertCurrency();
                                }
                              });
                            },
                          ),
                        ],
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),

                  // Currency Selection
                  Card(
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Column(
                        children: [
                          // From Currency
                          Row(
                            children: [
                              const Text('From:', style: TextStyle(fontWeight: FontWeight.bold)),
                              const SizedBox(width: 16),
                              Expanded(
                                child: DropdownButtonFormField<String>(
                                  value: _fromCurrency,
                                  decoration: InputDecoration(
                                    border: OutlineInputBorder(
                                      borderRadius: BorderRadius.circular(12),
                                    ),
                                  ),
                                  items: _currencies.map((currency) {
                                    return DropdownMenuItem(
                                      value: currency,
                                      child: Text(currency),
                                    );
                                  }).toList(),
                                  onChanged: (value) {
                                    if (value != null) {
                                      setState(() {
                                        _fromCurrency = value;
                                      });
                                      _convertCurrency();
                                    }
                                  },
                                ),
                              ),
                            ],
                          ),
                          const SizedBox(height: 16),

                          // Swap Button
                          Center(
                            child: IconButton(
                              onPressed: _swapCurrencies,
                              icon: const Icon(Icons.swap_vert),
                              tooltip: 'Swap currencies',
                              style: IconButton.styleFrom(
                                backgroundColor: Theme.of(context).colorScheme.primary,
                                foregroundColor: Theme.of(context).colorScheme.onPrimary,
                              ),
                            ),
                          ),
                          const SizedBox(height: 16),

                          // To Currency
                          Row(
                            children: [
                              const Text('To:', style: TextStyle(fontWeight: FontWeight.bold)),
                              const SizedBox(width: 16),
                              Expanded(
                                child: DropdownButtonFormField<String>(
                                  value: _toCurrency,
                                  decoration: InputDecoration(
                                    border: OutlineInputBorder(
                                      borderRadius: BorderRadius.circular(12),
                                    ),
                                  ),
                                  items: _currencies.map((currency) {
                                    return DropdownMenuItem(
                                      value: currency,
                                      child: Text(currency),
                                    );
                                  }).toList(),
                                  onChanged: (value) {
                                    if (value != null) {
                                      setState(() {
                                        _toCurrency = value;
                                      });
                                      _convertCurrency();
                                    }
                                  },
                                ),
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),

                  // Convert Button
                  ElevatedButton(
                    onPressed: _isLoading ? null : _convertCurrency,
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(vertical: 16),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                    ),
                    child: _isLoading
                        ? const SizedBox(
                            height: 20,
                            width: 20,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          )
                        : const Text('Convert', style: TextStyle(fontSize: 16)),
                  ),
                  const SizedBox(height: 24),

                  // Result
                  if (_errorMessage != null)
                    Card(
                      color: Colors.red.shade50,
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Row(
                          children: [
                            const Icon(Icons.error, color: Colors.red),
                            const SizedBox(width: 8),
                            Expanded(
                              child: Text(
                                _errorMessage!,
                                style: const TextStyle(color: Colors.red),
                              ),
                            ),
                          ],
                        ),
                      ),
                    )
                  else if (_currentRate != null)
                    Card(
                      color: Colors.green.shade50,
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            const Text(
                              'Conversion Result',
                              style: TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(height: 12),
                            Text(
                              _currentRate.toString(),
                              style: const TextStyle(
                                fontSize: 24,
                                fontWeight: FontWeight.bold,
                                color: Colors.green,
                              ),
                            ),
                            const SizedBox(height: 8),
                            Text(
                              'Exchange Rate: 1 ${_currentRate!.baseCurrency} = ${_currentRate!.rate.toStringAsFixed(4)} ${_currentRate!.targetCurrency}',
                              style: TextStyle(
                                fontSize: 14,
                                color: Colors.grey.shade700,
                              ),
                            ),
                            Text(
                              'Date: ${_currentRate!.date.day}/${_currentRate!.date.month}/${_currentRate!.date.year}',
                              style: TextStyle(
                                fontSize: 12,
                                color: Colors.grey.shade600,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                ],
              ),
            ),
    );
  }
}

6.2 Understanding the Currency Converter UI

Key Features:

  • Amount input: User enters the amount to convert
  • Currency dropdowns: Select from and to currencies
  • Swap button: Quickly swap the currencies
  • Auto-conversion: Converts automatically when user stops typing
  • Loading states: Shows spinner while loading
  • Error handling: Shows error messages when something goes wrong
  • Result display: Shows the converted amount and exchange rate

7. Integrating Currency Converter into the App

7.1 Add Currency Converter to Navigation

Update your main.dart to include the currency converter:

import 'package:flutter/material.dart';
import 'package:personal_finance_tracker/screens/currency_converter_screen.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 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',
          ),
        ],
      ),
    );
  }
}

8. Understanding Loading States and Error Handling

8.1 Types of Loading States

  1. Initial Loading: When the screen first loads
  2. Action Loading: When user performs an action (like converting currency)
  3. Background Loading: When refreshing data

8.2 Common Network Errors

try {
  final response = await http.get(url);
  // Handle response
} on TimeoutException catch (e) {
  // Network timeout
  print('Request timeout: $e');
} on SocketException catch (e) {
  // No internet connection
  print('No internet connection: $e');
} on HttpException catch (e) {
  // HTTP error
  print('HTTP error: $e');
} catch (e) {
  // Other errors
  print('Unknown error: $e');
}

8.3 Best Practices for Error Handling

  1. Show user-friendly messages: "Please check your internet connection" instead of "SocketException"
  2. Provide fallback options: Show cached data or default values
  3. Allow retry: Add retry buttons for failed requests
  4. Don't crash the app: Always handle errors gracefully

9. Key API Concepts Summary

9.1 HTTP Methods

  • GET: Retrieve data (like getting exchange rates)
  • POST: Send new data (like creating a user account)
  • PUT: Update existing data (like updating profile)
  • DELETE: Remove data (like deleting a transaction)

9.2 HTTP Status Codes

  • 200: OK - Request successful
  • 400: Bad Request - Invalid request format
  • 401: Unauthorized - Authentication required
  • 404: Not Found - Resource doesn't exist
  • 500: Internal Server Error - Server problem

9.3 JSON Structure

{
  "key": "value",
  "number": 123,
  "boolean": true,
  "array": ["item1", "item2"],
  "object": {
    "nested_key": "nested_value"
  }
}

9.4 Error Handling Strategies

  1. Try-Catch: Handle exceptions gracefully
  2. Timeouts: Don't wait forever for responses
  3. Fallbacks: Provide cached data when API fails
  4. User Feedback: Show appropriate error messages

10. Common Issues and Troubleshooting

10.1 Network Permission Issues

Problem: HTTP requests don't work on Android Solution: Add internet permission to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

10.2 JSON Parsing Errors

Problem: App crashes when parsing JSON Solution: Always validate JSON structure:

try {
  final data = json.decode(response.body);

  // Check if required fields exist
  if (data['rates'] != null && data['base'] != null) {
    // Process data
  } else {
    throw Exception('Invalid API response format');
  }
} catch (e) {
  debugPrint('JSON parsing error: $e');
}

10.3 Timeout Issues

Problem: App hangs waiting for API response Solution: Always use timeouts:

final response = await http.get(url).timeout(
  const Duration(seconds: 10),
  onTimeout: () {
    throw TimeoutException('Request timed out');
  },
);

12. Key Takeaways

  • APIs let your app get data from external services
  • HTTP requests are how you communicate with APIs
  • JSON is the common format for API data
  • Error handling is crucial for network operations
  • Loading states improve user experience
  • Caching provides offline functionality
  • Timeouts prevent app freezing
  • User feedback is important for network operations

12.1 Best Practices

  1. Always handle errors gracefully
  2. Show loading indicators for network operations
  3. Provide offline functionality when possible
  4. Use timeouts to prevent hanging
  5. Cache data to improve performance
  6. Validate JSON before processing
  7. Give user feedback about network status

Next Steps

In the next lab, we'll integrate Firebase for:

  • User authentication (sign up, login, logout)
  • Cloud storage for syncing transactions across devices
  • Real-time updates when data changes
  • Push notifications for important updates

Your app now has internet connectivity and can work with external APIs! This opens up many possibilities for enhanced functionality. The currency conversion feature makes your finance tracker more useful for users who deal with multiple currencies.

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!