Open In App

Expense Tracker Application Flutter

Last Updated : 26 Mar, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

In this tutorial, we will create a Flutter app that allows users to track their daily expenses and income, visualize spending patterns through charts, and store transaction data locally using Hive. This application will showcase how to handle user input, visualize data, and ensure local data persistence, providing users with a simple yet effective tool for managing their finances.

Application Features:

  • Add and categorize transactions as either income or expenses.
  • View and manage a list of all past transactions.
  • Visualize spending trends with dynamic charts.
  • Store and retrieve transaction data locally using Hive.

Below is the demo video for the app that we will be developing.


Setting Up the Project

Step 1 : Create a new Flutter Application

Create a new Flutter application using the command Prompt. To create a new app, write the below command and run it.

flutter create app_name

To know more about it refer this article: Creating a Simple Application in Flutter

Step 2 : Adding the Dependency

For this project, you’ll need these dependencies:

  1. hive_flutter: To store and manage transaction data locally.
  2. intl: For formatting dates within the application.
  3. fl_chart: To create dynamic charts for visualizing expenses over time.

hive_generator and build_runner are the dev_dependencies to generate .g.dart files.

To add the dependency to the pubspec.yaml file, add hive_flutter, fl_chart and intl  as a dependency in the dependencies and hive_generator and build_runner as dev_dependencies part of the pubspec.yaml file, as shown below:

Dart
dependencies:
    flutter:
      sdk: flutter
    hive_flutter: ^1.1.0
    fl_chart: ^0.70.2
    intl: ^0.20.2

dev_dependencies:
    flutter_test:
      sdk: flutter
    hive_generator:
    build_runner:

Now run the below command in the terminal.

flutter pub get

Or

Run the below command in the terminal.

flutter pub add hive_flutter fl_chart intl 

Folder Structure

Create the following folder structure for better organization of your files:

folder_structure


This structure keeps database logic, models, and UI separated, making the code modular and easy to maintain.

Build the Application

The Application is divided into the following packages:

  • Models: Contains Objects that encapsulate the data and behavior of the application domain.
  • UI elements: Contains Code that defines the UI of the Application.

Step 1: Models

Let's start by creating a transaction model for our transaction data in trancs.dart.

trancs.dart:

Dart
// Importing Hive package for local storage
import 'package:hive/hive.dart'; 

// Part statement is used for code
// generation for the Transaction class.
part 'trancs.g.dart'; 

// Defining a Hive Type with a unique
// typeId of 0 to store Transaction objects.
@HiveType(typeId: 0)
class Transaction extends HiveObject {
      
      // Field to store the category of the
      // transaction (e.g., Food, Transportation).
      @HiveField(0)
      final String category;
    
      // Field to store the transaction amount.
      @HiveField(1)
      final double amount;
    
      // Field to store whether the transaction
      // is an income (true) or an expense (false).
      @HiveField(2)
      final bool isIncome;
    
      // Field to store the date of the transaction.
      @HiveField(3)
      final DateTime date;
    
      // Constructor for the Transaction class
      // with required fields.
      Transaction({
        required this.category,
        required this.amount,
        required this.isIncome,
        required this.date,
      });
}


Now you need to generate trancs.g.dart file , for that run the command in your terminal :

dart run build_runner build

after_generation


Step 2 : UI Elements

Our Application contains three main UI component screens as mentioned below:

  • Transaction Screen
  • Add Transactions Screen
  • Reports Screen

Now let us Observe all of them.

- Transaction Screen (Display Transitions)

The tranc_screen.dart is where users will see the list of past transactions he/she made :

tranc_screen.dart:

tranc_screen.dart
import 'package:flutter/material.dart';
import 'package:geeks_for_geeks/models/trancs.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';

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

  @override
  Widget build(BuildContext context) {
    
    // Retrieve the Hive box where
    // transactions are stored
    final transactionBox = Hive.box('transactions');

    return Scaffold(
      body: ValueListenableBuilder(
        
        // Listens to changes in the transactions
        // box and rebuilds UI accordingly
        valueListenable: transactionBox.listenable(),
        builder: (context, Box box, widget) {
          
          // Display message when there are no transactions
          if (box.isEmpty) {
            return const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    
                    // Icon indicating no transactions
                    Icons.money_off, 
                    size: 80,
                  ),
                  SizedBox(height: 16),
                  Text(
                    'No transactions yet.',
                    
                    // Optionally style this text using theme later
                  ),
                ],
              ),
            );
          } else {
              
            // If transactions exist, display them in a ListView
            return ListView.builder(
              padding: const EdgeInsets.all(16),
              
              // Number of transactions
              itemCount: box.length, 
              itemBuilder: (context, index) {
                
                // Retrieve the transaction at the current index
                final transaction = box.getAt(index) as Transaction;
                
                // Check if it's an income
                final isIncome = transaction.isIncome;
                
                // Format the date
                final formattedDate = DateFormat.yMMMd()
                    .format(transaction.date); 

                // Display each transaction inside a card
                return Card(
                  margin: const EdgeInsets.symmetric(vertical: 8),
                  elevation: 3,
                  child: ListTile(
                    leading: Icon(
                      isIncome
                          ? Icons.arrow_upward
                          : Icons.arrow_downward, // Icon depending on transaction type
                      color: isIncome
                          ? Colors.green
                          : Colors.red, // Color for income/expense
                    ),
                    title: Text(
                      // Transaction category name
                      transaction.category, 
                      
                      // style: theme.textTheme.subtitle1, // Optional text styling
                    ),
                    
                    // Formatted transaction date
                    subtitle: Text(formattedDate), 
                    trailing: Text(
                      isIncome
                          ? '+ \$${transaction.amount.toStringAsFixed(2)}' // Display income with plus sign
                          : '- \$${transaction.amount.toStringAsFixed(2)}', // Display expense with minus sign
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        color: isIncome
                            ? Colors.green
                            : Colors.red, // Color based on transaction type
                        fontSize: 16,
                      ),
                    ),
                  ),
                );
              },
            );
          }
        },
      ),
    );
  }
}


Screenshot of the Transaction screen :

transaction_screen


- Add Transaction Screen

The add_tranc.dart is where users can add transactions by entering values like amount, category, Income/Expense, and date :

add_tranc.dart
import 'package:flutter/material.dart';
import 'package:geeks_for_geeks/models/trancs.dart';
import 'package:hive/hive.dart';
import 'package:intl/intl.dart';

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

  @override
  _AddTransactionScreenState createState() => _AddTransactionScreenState();
}

class _AddTransactionScreenState extends State<AddTransactionScreen> {
  
  // Form key for validation
  final _formKey = GlobalKey<FormState>(); 
  
  // Default category
  String _category = 'Food'; 
  TextEditingController _amountController = TextEditingController();
  bool _isIncome = false;
  
  // Default date as current date
  DateTime _selectedDate = DateTime.now(); 

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          // Form with validation
          key: _formKey, 
          child: ListView(
            children: [
                
              // Input for transaction amount
              TextFormField(
                controller: _amountController,
                decoration: const InputDecoration(
                  labelText: 'Amount',
                  border: OutlineInputBorder(),
                  icon: Icon(Icons.attach_money),
                ),
                keyboardType: TextInputType.number,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                      
                    // Validate amount
                    return 'Please enter an amount'; 
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),

              // Dropdown for selecting category
              DropdownButtonFormField<String>(
                value: _category,
                decoration: const InputDecoration(
                  labelText: 'Category',
                  border: OutlineInputBorder(),
                  icon: Icon(Icons.category),
                ),
                onChanged: (String? newValue) {
                  setState(() {
                    _category = newValue!;
                  });
                },
                items: <String>['Food', 'Transportation', 'Entertainment']
                    .map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem<String>(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              ),
              const SizedBox(height: 16),

              // Switch for Income/Expense selection
              SwitchListTile(
                title: const Text('Income'),
                value: _isIncome,
                onChanged: (bool value) {
                  setState(() {
                    _isIncome = value;
                  });
                },
                secondary: Icon(
                  _isIncome ? Icons.arrow_upward : Icons.arrow_downward,
                  color: _isIncome ? Colors.green : Colors.red,
                ),
              ),
              const SizedBox(height: 16),

              // Date picker for transaction date
              ListTile(
                title: Text(
                  'Date: ${DateFormat('yyyy-MM-dd').format(_selectedDate)}',
                ),
                trailing: const Icon(Icons.calendar_today),
                
                // Opens date picker
                onTap: () => _selectDate(context), 
              ),
              const SizedBox(height: 16),

              // Button to add the transaction
              ElevatedButton.icon(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.green,
                  foregroundColor: Colors.white,
                ),
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                      
                    // Save form data
                    _formKey.currentState!.save(); 

                    // Create and save transaction
                    final transaction = Transaction(
                      category: _category,
                      amount: _amountController.text.isNotEmpty
                          ? double.parse(_amountController.text)
                          : 0.0,
                      isIncome: _isIncome,
                      date: _selectedDate,
                    );
                    Hive.box('transactions').add(transaction);

                    // Reset form after submission
                    setState(() {
                      _category = 'Food';
                      _amountController.clear();
                      _isIncome = false;
                      _selectedDate = DateTime.now();
                    });
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('Transaction added'),
                      ),
                    );
                  }
                },
                icon: const Icon(
                  Icons.add,
                  color: Colors.white,
                ),
                label: const Text('Add Transaction'),
              ),
            ],
          ),
        ),
      ),
    );
  }

  // Date picker function
  Future<void> _selectDate(BuildContext context) async {
    final picked = await showDatePicker(
      context: context,
      initialDate: _selectedDate,
      firstDate: DateTime(2000),
      lastDate: DateTime(2101),
    );
    if (picked != null && picked != _selectedDate) {
      setState(() {
        _selectedDate = picked; // Update selected date
      });
    }
  }
}


Screenshot of the Add Transaction Screen :

add_transaction_screen


- Report Screen (Graph)

This report.dart screen displays a graph of Expenses,

report.dart
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'package:hive/hive.dart';
import '../models/trancs.dart';

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

  @override
  Widget build(BuildContext context) {
    final transactionBox = Hive.box('transactions');
    final List<Transaction> transactions =
        transactionBox.values.toList().cast<Transaction>();

    // Group expenses by date
    Map<String, double> dailyExpenses = {};
    transactions.where((txn) => !txn.isIncome).forEach((txn) {
      String dateKey = DateFormat('yyyy-MM-dd').format(txn.date);
      dailyExpenses.update(dateKey, (value) => value + txn.amount,
          ifAbsent: () => txn.amount);
    });

    // Create spots for graph based on daily expenses
    List<FlSpot> expenseSpots = [];
    List<String> dateLabels = [];
    int index = 0;

    dailyExpenses.forEach((date, amount) {
      expenseSpots.add(FlSpot(index.toDouble(), amount));
      dateLabels.add(date);
      index++;
    });

    // Calculate max expense value for Y-axis limit
    double maxExpense = dailyExpenses.isNotEmpty
        ? dailyExpenses.values.reduce((a, b) => a > b ? a : b)
        : 100;
    double interval =
        (maxExpense / 5).ceilToDouble(); // Dynamic interval for better scaling

    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: LineChart(
          LineChartData(
            titlesData: FlTitlesData(
              bottomTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  reservedSize: 40,
                  getTitlesWidget: (value, meta) {
                    int index = value.toInt();
                    if (index >= 0 && index < dateLabels.length) {
                      return Text(
                        DateFormat('MM/dd').format(
                            DateFormat('yyyy-MM-dd').parse(dateLabels[index])),
                        style: const TextStyle(fontSize: 10),
                        textAlign: TextAlign.center,
                      );
                    }
                    return const Text('');
                  },
                ),
              ),
              leftTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  interval: interval, // Set dynamic interval for Y-axis
                  reservedSize: 40,
                  getTitlesWidget: (value, meta) {
                    return Text('\$${value.toInt()}',
                        style: const TextStyle(fontSize: 10));
                  },
                ),
              ),
              topTitles:
                  const AxisTitles(sideTitles: SideTitles(showTitles: false)),
              rightTitles:
                  const AxisTitles(sideTitles: SideTitles(showTitles: false)),
            ),
            gridData: FlGridData(
              show: true,
              drawVerticalLine: true,
              horizontalInterval: interval,
              getDrawingHorizontalLine: (value) {
                return FlLine(
                  color: Colors.grey.withOpacity(0.5),
                  strokeWidth: 1,
                  dashArray: [5, 5],
                );
              },
            ),
            lineBarsData: [
              LineChartBarData(
                spots: expenseSpots, // Set expense data points
                isCurved: true,
                color: Colors.redAccent,
                barWidth: 4,
                dotData:
                    const FlDotData(show: true), // Show dots on each data point
                belowBarData: BarAreaData(
                  show: true,
                  color: Colors.redAccent
                      .withOpacity(0.2), // Light red area under the curve
                ),
              ),
            ],
            borderData: FlBorderData(
              show: true,
              border: Border.all(color: Colors.black.withOpacity(0.3)),
            ),
            minY: 0,
            maxY: maxExpense +
                (interval * 2), // Add extra space above the highest expense
          ),
        ),
      ),
    );
  }
}

Screenshot of the Reports Screen :

reports_screen


- Expense Home Screen

This expence_home_screen.dart screen is called from the main.dart file, and shows a Bottomtab :

expence_home_screen.dart
import 'package:flutter/material.dart';
import 'package:geeks_for_geeks/screens/add_tranc.dart';
import 'package:geeks_for_geeks/screens/report.dart';
import 'package:geeks_for_geeks/screens/transc_screen.dart';

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

  @override
  _ExpenseHomeScreenState createState() => _ExpenseHomeScreenState();
}

class _ExpenseHomeScreenState extends State<ExpenseHomeScreen> {
  int _selectedIndex = 0;

  final List<Widget> _screens = [
    const TransactionScreen(),
    const AddTransactionScreen(),
    const ReportsScreen(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text('GFG Expense Tracker'),
        backgroundColor: Colors.green,
        foregroundColor: Colors.white,
      ),
      body: _screens[_selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        backgroundColor: Colors.green,
        selectedItemColor: Colors.white,
        unselectedItemColor: Colors.white.withOpacity(0.6),
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            label: 'Transactions',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.add),
            label: 'Add Transaction',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.bar_chart),
            label: 'Reports',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
      ),
    );
  }
}


Main Application File Code

After successfully creating all the files mentioned above we can finally utilize them to create a fully functional Application.

Finally, let's Call Our ExpenseHomeScreen from the main.dart file

main.dart
import 'package:flutter/material.dart';
import 'package:geeks_for_geeks/models/trancs.dart';
import 'package:geeks_for_geeks/screens/expence_home_screen.dart';
import 'package:hive_flutter/hive_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Hive
  await Hive.initFlutter();

  // Register the TransactionAdapter
  Hive.registerAdapter(TransactionAdapter());

  // Open the Hive box
  await Hive.openBox('transactions');

  runApp(const ExpenseTrackerApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'GFG Expense Tracker',
      home: ExpenseHomeScreen(),
    );
  }
}


Click Here to access complete Application Code

Output:



Next Article

Similar Reads