Building a Simple Budget Tracker App: Architecture and Design
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:
- Budget limits per category — alert when spending exceeds the limit
- Recurring transactions — rent, salary, subscriptions
- CSV export — users want their data portable
- Cloud sync — add Firebase for multi-device access
- 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.