← Articles

Building a Simple Budget Tracker App: Architecture and Design

By Mark · 29 June 20260 views

Building a Simple Budget Tracker App: Architecture and Design

A budget tracker is one of the best apps to build when learning mobile development — it touches enough real problems (data persistence, CRUD, charting, categories, date handling) to be genuinely instructive without being overwhelming. This article walks through the architecture and design decisions for a well-structured Flutter budget tracker.

Feature Scope

For a first version, keep scope tight:

  • Add income and expense transactions with amount, category, date, and optional note
  • View a list of recent transactions, filterable by month
  • See a summary of income vs expense and the balance
  • Visualize spending by category as a pie chart
  • Persist data locally (no backend required for v1)

Data Model

enum TransactionType { income, expense }

class Transaction {
  final String id;
  final TransactionType type;
  final double amount;
  final String category;
  final DateTime date;
  final String? note;

  Transaction({
    required this.id,
    required this.type,
    required this.amount,
    required this.category,
    required this.date,
    this.note,
  });
}

The category is a string that matches a predefined list (Food, Transport, Utilities, Shopping, Entertainment, etc.). Avoid enums for categories — users may want custom categories in a future version.

Storage Layer

For local persistence, use isar for its fast queries and type-safe schema:

@collection
class TransactionEntity {
  Id get isarId => Isar.autoIncrement;

  @Index()
  late String id;

  late String type; // 'income' or 'expense'
  late double amount;
  late String category;
  late DateTime date;
  String? note;
}

Abstract storage behind a repository interface so you can swap implementations (e.g., add cloud sync later without touching business logic):

abstract class TransactionRepository {
  Future<List<Transaction>> getByMonth(int year, int month);
  Future<void> save(Transaction transaction);
  Future<void> delete(String id);
  Stream<List<Transaction>> watchByMonth(int year, int month);
}

State Management with Riverpod

Organize providers around the features:

@riverpod
class SelectedMonth extends _$SelectedMonth {
  @override
  DateTime build() => DateTime.now();

  void previous() => state = DateTime(state.year, state.month - 1);
  void next()     => state = DateTime(state.year, state.month + 1);
}

@riverpod
Stream<List<Transaction>> monthTransactions(MonthTransactionsRef ref) {
  final month = ref.watch(selectedMonthProvider);
  final repo   = ref.watch(transactionRepositoryProvider);
  return repo.watchByMonth(month.year, month.month);
}

@riverpod
BudgetSummary summary(SummaryRef ref) {
  final transactions = ref.watch(monthTransactionsProvider).valueOrNull ?? [];
  final income  = transactions
      .where((t) => t.type == TransactionType.income)
      .fold(0.0, (sum, t) => sum + t.amount);
  final expense = transactions
      .where((t) => t.type == TransactionType.expense)
      .fold(0.0, (sum, t) => sum + t.amount);
  return BudgetSummary(income: income, expense: expense);
}

Screen Structure

screens/
  home/
    home_screen.dart       ← Summary card + transaction list
    widgets/
      summary_card.dart
      transaction_list.dart
      transaction_tile.dart
  add_transaction/
    add_transaction_screen.dart
    widgets/
      amount_input.dart
      category_picker.dart
  analytics/
    analytics_screen.dart
    widgets/
      spending_pie_chart.dart
      category_bar.dart

The Add Transaction Form

Validation is important here — amount must be positive, category must be selected, date must not be in the future (for expense tracking):

class AddTransactionForm extends ConsumerStatefulWidget {
  @override
  ConsumerState<AddTransactionForm> createState() => _AddTransactionFormState();
}

class _AddTransactionFormState extends ConsumerState<AddTransactionForm> {
  final _formKey = GlobalKey<FormState>();
  final _amountController = TextEditingController();
  String? _selectedCategory;
  TransactionType _type = TransactionType.expense;
  DateTime _date = DateTime.now();

  Future<void> _submit() async {
    if (!_formKey.currentState!.validate()) return;
    final transaction = Transaction(
      id: const Uuid().v4(),
      type: _type,
      amount: double.parse(_amountController.text),
      category: _selectedCategory!,
      date: _date,
    );
    await ref.read(transactionRepositoryProvider).save(transaction);
    if (mounted) Navigator.of(context).pop();
  }

  // ... build method with Form, TextFormField, DropdownButton, DatePicker
}

Charting

Use the fl_chart package for the pie chart. Group transactions by category and map to PieChartSectionData:

final sections = categoryTotals.entries.map((entry) {
  return PieChartSectionData(
    value: entry.value,
    title: entry.key,
    color: categoryColor(entry.key),
    radius: 80,
  );
}).toList();

What to Build Next

Once v1 is working:

  1. Budget limits per category — alert when spending exceeds the limit
  2. Recurring transactions — rent, salary, subscriptions
  3. CSV export — users want their data portable
  4. Cloud sync — add Firebase for multi-device access
  5. Widgets — home screen widget showing the monthly balance

Conclusion

The budget tracker is an ideal learning project because the architecture principles it demands — a clean repository layer, reactive state management, a separation between UI and business logic — are the same principles you will use in production apps of any size. Build it well and you will have both a useful tool and a reference architecture to apply everywhere.

Sign in to like, dislike, or report.

Comments

No comments yet. Be the first!

Sign in to leave a comment.

Building a Simple Budget Tracker App: Architecture and Design — ANN Tech