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 Flutterpath: Helps us find the right location to store the database fileshared_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 identifiertitle- Transaction descriptionamount- Money amount (as number)date- When the transaction happenedcategory- 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:
- Singleton Pattern: Only one database connection exists in the entire app
- Async/Await: Database operations take time, so we use async functions
- CRUD Operations: Create, Read, Update, Delete - the four basic database operations
- SQL Queries: We use SQL commands to interact with the database
Important Methods:
insertTransaction(): Adds a new transaction to the databasegetAllTransactions(): Gets all transactions from the databaseupdateTransaction(): Modifies an existing transactiondeleteTransaction(): 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 ininitState() - 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.