image

22 Jan 2026

9K

35K

Building an Expense Tracker Widget with a Chart in Flutter

In today's fast-paced world, managing personal finances is crucial. An expense tracker helps individuals monitor their spending habits, identify areas for savings, and achieve financial goals. While simply listing expenses is helpful, visualizing spending patterns through charts provides deeper insights and makes the data more digestible. This article will guide you through building a professional expense tracker widget in Flutter, complete with an interactive chart to display spending trends.

Why Flutter for an Expense Tracker?

Flutter, Google's UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, is an excellent choice for this project. Its declarative UI, hot reload feature, and rich set of widgets enable rapid development and create beautiful, high-performance applications.

Key Components of Our Expense Tracker

  • Expense Model: A data structure to represent individual expenses.
  • State Management: To efficiently manage and update the list of expenses across the application. We'll use Provider for simplicity and scalability.
  • Expense Input Form: A UI to add new expenses.
  • Expense List: To display all recorded expenses.
  • Charting Widget: To visualize spending data. We'll utilize the popular fl_chart package for creating elegant bar charts.

Setting Up Your Flutter Project

First, create a new Flutter project and add the necessary dependencies to your pubspec.yaml file.


flutter create expense_tracker_app
cd expense_tracker_app

Update pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  intl: ^0.18.1 # For date formatting
  provider: ^6.0.5 # For state management
  fl_chart: ^0.65.0 # For charting capabilities

After adding, run flutter pub get to fetch the packages.

1. Defining the Expense Model

Let's create a simple model for our expense. Each expense will have an ID, title, amount, and date.


// lib/models/expense.dart
import 'package:flutter/foundation.dart';

class Expense {
  final String id;
  final String title;
  final double amount;
  final DateTime date;

  Expense({
    required this.id,
    required this.title,
    required this.amount,
    required this.date,
  });
}

2. State Management with Provider

We'll use ChangeNotifier and Provider to manage our list of expenses. This allows us to easily add new expenses and notify our UI to rebuild when the data changes.


// lib/providers/expense_provider.dart
import 'package:flutter/material.dart';
import '../models/expense.dart';

class ExpenseProvider with ChangeNotifier {
  final List<Expense> _expenses = [];

  List<Expense> get expenses {
    return [..._expenses]; // Return a copy to prevent external modification
  }

  // Method to add a new expense
  void addExpense(String title, double amount, DateTime date) {
    final newExpense = Expense(
      id: DateTime.now().toString(),
      title: title,
      amount: amount,
      date: date,
    );
    _expenses.add(newExpense);
    notifyListeners(); // Notify all listening widgets
  }

  // Method to get expenses for the last 7 days
  List<Expense> get recentExpenses {
    return _expenses.where((expense) {
      return expense.date.isAfter(DateTime.now().subtract(const Duration(days: 7)));
    }).toList();
  }

  // Method to group recent expenses by day for chart data
  List<Map<String, Object>> get groupedDailyExpenses {
    return List.generate(7, (index) {
      final weekDay = DateTime.now().subtract(Duration(days: index));
      double totalSum = 0.0;

      for (var i = 0; i < recentExpenses.length; i++) {
        if (recentExpenses[i].date.day == weekDay.day &&
            recentExpenses[i].date.month == weekDay.month &&
            recentExpenses[i].date.year == weekDay.year) {
          totalSum += recentExpenses[i].amount;
        }
      }

      return {
        'day': weekDay,
        'amount': totalSum,
      };
    }).reversed.toList(); // Reverse to show latest day on the right
  }
}

Wrap your MaterialApp with ChangeNotifierProvider in main.dart:


// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';

import './providers/expense_provider.dart';
import './screens/expense_tracker_screen.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (ctx) => ExpenseProvider(),
      child: MaterialApp(
        title: 'Personal Expenses',
        theme: ThemeData(
          primarySwatch: Colors.purple,
          hintColor: Colors.amber,
          fontFamily: 'Quicksand',
          textTheme: ThemeData.light().textTheme.copyWith(
            titleLarge: const TextStyle(
              fontFamily: 'OpenSans',
              fontWeight: FontWeight.bold,
              fontSize: 18,
            ),
          ),
          appBarTheme: const AppBarTheme(
            titleTextStyle: TextStyle(
              fontFamily: 'OpenSans',
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        home: const ExpenseTrackerScreen(),
      ),
    );
  }
}

3. Creating the Expense Input Form

This widget will allow users to input new expenses.


// lib/widgets/new_expense.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';

import '../providers/expense_provider.dart';

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

  @override
  State<NewExpense> createState() => _NewExpenseState();
}

class _NewExpenseState extends State<NewExpense> {
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();
  DateTime? _selectedDate;

  void _presentDatePicker() {
    showDatePicker(
      context: context,
      initialDate: DateTime.now(),
      firstDate: DateTime(2023),
      lastDate: DateTime.now(),
    ).then((pickedDate) {
      if (pickedDate == null) {
        return;
      }
      setState(() {
        _selectedDate = pickedDate;
      });
    });
  }

  void _submitData() {
    if (_amountController.text.isEmpty) {
      return;
    }
    final enteredTitle = _titleController.text;
    final enteredAmount = double.parse(_amountController.text);

    if (enteredTitle.isEmpty || enteredAmount <= 0 || _selectedDate == null) {
      return;
    }

    Provider.of<ExpenseProvider>(context, listen: false).addExpense(
      enteredTitle,
      enteredAmount,
      _selectedDate!,
    );

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

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 5,
      child: Container(
        padding: const EdgeInsets.all(10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: <Widget>[
            TextField(
              decoration: const InputDecoration(labelText: 'Title'),
              controller: _titleController,
              onSubmitted: (_) => _submitData(),
            ),
            TextField(
              decoration: const InputDecoration(labelText: 'Amount'),
              controller: _amountController,
              keyboardType: TextInputType.number,
              onSubmitted: (_) => _submitData(),
            ),
            SizedBox(
              height: 70,
              child: Row(
                children: <Widget>[
                  Expanded(
                    child: Text(
                      _selectedDate == null
                          ? 'No Date Chosen!'
                          : 'Picked Date: ${DateFormat.yMd().format(_selectedDate!)}',
                    ),
                  ),
                  TextButton(
                    onPressed: _presentDatePicker,
                    child: const Text(
                      'Choose Date',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                  )
                ],
              ),
            ),
            ElevatedButton(
              onPressed: _submitData,
              style: ElevatedButton.styleFrom(
                backgroundColor: Theme.of(context).primaryColor,
                foregroundColor: Colors.white,
              ),
              child: const Text('Add Expense'),
            ),
          ],
        ),
      ),
    );
  }
}

4. Integrating the Chart (fl_chart)

We'll create a Chart widget that uses fl_chart to display a bar chart of daily expenses over the last 7 days.


// lib/widgets/chart.dart
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';

import '../providers/expense_provider.dart';
import 'package:provider/provider.dart';

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

  List<BarChartGroupData> get barGroups {
    return Provider.of<ExpenseProvider>(context).groupedDailyExpenses.map((data) {
      return BarChartGroupData(
        x: (data['day'] as DateTime).weekday, // Use weekday as x-axis value
        barRods: [
          BarChartRodData(
            toY: data['amount'] as double,
            color: Theme.of(context).primaryColor,
            width: 10,
            borderRadius: BorderRadius.circular(2),
          ),
        ],
        showingTooltipIndicators: [0], // Show tooltip for the first rod (our only rod)
      );
    }).toList();
  }

  double get maxSpending {
    return Provider.of<ExpenseProvider>(context).groupedDailyExpenses.fold(0.0, (sum, item) {
      return sum + (item['amount'] as double);
    });
  }

  @override
  Widget build(BuildContext context) {
    final expenseProvider = Provider.of<ExpenseProvider>(context);
    final groupedTransactionValues = expenseProvider.groupedDailyExpenses;

    return Card(
      elevation: 6,
      margin: const EdgeInsets.all(20),
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: SizedBox(
          height: 150, // Fixed height for the chart
          child: BarChart(
            BarChartData(
              barGroups: barGroups,
              alignment: BarChartAlignment.spaceAround,
              maxY: maxSpending == 0 ? 100 : maxSpending * 1.1, // Adjust max Y
              gridData: const FlGridData(show: false),
              titlesData: FlTitlesData(
                bottomTitles: AxisTitles(
                  sideTitles: SideTitles(
                    showTitles: true,
                    getTitlesWidget: (value, meta) {
                      final day = groupedTransactionValues.firstWhere(
                        (element) => (element['day'] as DateTime).weekday == value.toInt(),
                        orElse: () => {'day': DateTime.now(), 'amount': 0.0},
                      );
                      return SideTitleWidget(
                        axisSide: meta.axisSide,
                        space: 4,
                        child: Text(DateFormat.E().format(day['day'] as DateTime)), // 'E' for short day name
                      );
                    },
                  ),
                ),
                leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
                topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
                rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
              ),
              borderData: FlBorderData(
                show: false,
              ),
              barTouchData: BarTouchData(
                touchTooltipData: BarTouchTooltipData(
                  tooltipBgColor: Colors.blueGrey,
                  getTooltipItem: (group, groupIndex, rod, rodIndex) {
                    final day = groupedTransactionValues.firstWhere(
                      (element) => (element['day'] as DateTime).weekday == group.x.toInt(),
                      orElse: () => {'day': DateTime.now(), 'amount': 0.0},
                    );
                    return BarTooltipItem(
                      '${DateFormat.MMMEd().format(day['day'] as DateTime)}\n', // Full date
                      const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
                      children: <TextSpan>[
                        TextSpan(
                          text: '\$${rod.toY.toStringAsFixed(2)}',
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 14,
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                      ],
                    );
                  },
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

5. Displaying the Expense List

This widget will display the list of added expenses.


// lib/widgets/expense_list.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';

import '../providers/expense_provider.dart';

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

  @override
  Widget build(BuildContext context) {
    final expenseData = Provider.of<ExpenseProvider>(context);
    final expenses = expenseData.expenses;

    return expenses.isEmpty
        ? LayoutBuilder(
            builder: (ctx, constraints) {
              return Column(
                children: <Widget>[
                  Text(
                    'No expenses added yet!',
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  const SizedBox(height: 20),
                  SizedBox(
                    height: constraints.maxHeight * 0.6,
                    child: Image.asset(
                      'assets/images/waiting.png', // Make sure you have this image in your assets
                      fit: BoxFit.cover,
                    ),
                  ),
                ],
              );
            },
          )
        : ListView.builder(
            itemCount: expenses.length,
            itemBuilder: (ctx, index) {
              return Card(
                margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 5),
                elevation: 5,
                child: ListTile(
                  leading: CircleAvatar(
                    radius: 30,
                    child: Padding(
                      padding: const EdgeInsets.all(6),
                      child: FittedBox(
                        child: Text('\$${expenses[index].amount.toStringAsFixed(2)}'),
                      ),
                    ),
                  ),
                  title: Text(
                    expenses[index].title,
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  subtitle: Text(
                    DateFormat.yMMMd().format(expenses[index].date),
                  ),
                ),
              );
            },
          );
  }
}

Don't forget to add an asset folder for your image. In pubspec.yaml:


flutter:
  uses-material-design: true
  assets:
    - assets/images/waiting.png

And place an image named waiting.png inside the assets/images folder.

6. Assembling the Expense Tracker Screen

Finally, we combine all the pieces into our main screen widget.


// lib/screens/expense_tracker_screen.dart
import 'package:flutter/material.dart';

import '../widgets/new_expense.dart';
import '../widgets/expense_list.dart';
import '../widgets/chart.dart';

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

  void _startAddNewExpense(BuildContext ctx) {
    showModalBottomSheet(
      context: ctx,
      builder: (_) {
        return GestureDetector(
          onTap: () {}, // Prevents closing when tapping on the bottom sheet content
          behavior: HitTestBehavior.opaque,
          child: const NewExpense(),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Personal Expenses'),
        actions: <Widget>[
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: () => _startAddNewExpense(context),
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            const Chart(), // Our expense chart
            SizedBox(
              height: MediaQuery.of(context).size.height * 0.6, // Adjust height as needed
              child: const ExpenseList(), // Our list of expenses
            ),
          ],
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      floatingActionButton: FloatingActionButton(
        onPressed: () => _startAddNewExpense(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Running the Application

You can now run your Flutter application:


flutter run

You should see an app with an AppBar, a chart displaying recent expenses (initially empty), a list of expenses (also empty), and an add button. Tapping the add button will bring up a bottom sheet to add new expenses. As you add expenses, the list and the chart will update dynamically.

Further Enhancements

  • Persistence: Integrate a local database (e.g., sqflite or Hive) to save expenses permanently.
  • Delete/Edit Expense: Add functionality to remove or modify existing expenses.
  • Filtering and Categories: Allow users to filter expenses by date range, category (e.g., food, transport, entertainment), or payment method.
  • Multiple Chart Types: Implement a pie chart for category breakdowns or a line chart for long-term trends.
  • User Authentication: For a multi-user application, implement user authentication and cloud synchronization.

Conclusion

Building an expense tracker with a chart in Flutter is a practical project that showcases several core Flutter concepts, including state management, custom widgets, and third-party package integration. By following this guide, you've created a functional and visually appealing application that helps users gain better control over their finances. The modular design allows for easy extension and addition of more advanced features, making it a solid foundation for more complex financial applications.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is