Skip to content

flutter_building_complete_application

在本章中,我们将整合之前学习到的许多概念,构建一个更完整的应用:一个记账应用(Expense Tracker)。这个应用将允许用户记录支出、查看支出列表、查看总支出金额,并可能编辑/删除条目。

我们将使用:

  • 核心 Flutter Widget(Scaffold、ListView、ListTile、FloatingActionButton、Form、TextFormField)。
  • sqflite 用于本地数据持久化。
  • provider 用于状态管理。
  • 使用 intl 包进行日期格式化。
  • 基本的表单验证。
  1. 创建一个新的 Flutter 项目:flutter create expense_tracker_app

  2. 添加依赖到 pubspec.yaml(使用最新版本):

  3. 运行 flutter pub get。

定义 Expense 类。

import 'package:intl/intl.dart';
class Expense {
final int? id; // 插入时可能为 null
final double amount;
final DateTime date;
final String category;
Expense({
this.id,
required this.amount,
required this.date,
required this.category,
});
// 用于格式化日期的辅助 getter
String get formattedDate {
return DateFormat('yyyy-MM-dd').format(date);
}
// 将 Expense 对象转换为用于 sqflite 的 Map
Map<String, dynamic> toMap() {
return {
'id': id,
'amount': amount,
// 在数据库中将日期存储为 ISO8601 字符串
'date': date.toIso8601String(),
'category': category,
};
}
// 从 Map 创建 Expense 对象
factory Expense.fromMap(Map<String, dynamic> map) {
return Expense(
id: map['id'] as int?,
amount: map['amount'] as double,
// 将 ISO8601 字符串解析回 DateTime
date: DateTime.parse(map['date'] as String),
category: map['category'] as String,
);
}
}

2. 数据库 Helper(lib/database_helper.dart)

Section titled “2. 数据库 Helper(lib/database_helper.dart)”

创建一个单例 DatabaseHelper,类似于数据持久化章节中的 Helper,但针对 Expense 模型和表进行了调整。

import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'expense.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
static Database? _database;
DatabaseHelper._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDb();
return _database!;
}
Future<Database> _initDb() async {
String path = join(await getDatabasesPath(), 'expense_database.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount REAL NOT NULL,
date TEXT NOT NULL,
category TEXT NOT NULL
)
''');
// Optional: Insert sample data
await db.insert('expenses', {'amount': 15.50, 'date': DateTime.now().toIso8601String(), 'category': 'Food'});
}
Future<int> insertExpense(Expense expense) async {
final db = await database;
Map<String, dynamic> row = expense.toMap();
row.remove('id'); // 让 SQLite 处理自增
return await db.insert('expenses', row);
}
Future<List<Expense>> getAllExpenses() async {
final db = await database;
// 按日期降序排列(最新优先)
final List<Map<String, dynamic>> maps = await db.query('expenses', orderBy: 'date DESC');
return List.generate(maps.length, (i) => Expense.fromMap(maps[i]));
}
Future<int> updateExpense(Expense expense) async {
final db = await database;
return await db.update('expenses', expense.toMap(), where: 'id = ?', whereArgs: [expense.id]);
}
Future<int> deleteExpense(int id) async {
final db = await database;
return await db.delete('expenses', where: 'id = ?', whereArgs: [id]);
}
// 可选:计算总支出的方法
Future<double> getTotalExpenses() async {
final db = await database;
final result = await db.rawQuery('SELECT SUM(amount) as total FROM expenses');
double total = result.first['total'] != null ? (result.first['total'] as num).toDouble() : 0.0;
return total;
}
}

3. 使用 Provider 进行状态管理(lib/expense_provider.dart)

Section titled “3. 使用 Provider 进行状态管理(lib/expense_provider.dart)”

创建一个 ChangeNotifier 来管理支出列表并通知监听者更改。

import 'package:flutter/foundation.dart';
import 'database_helper.dart';
import 'expense.dart';
class ExpenseProvider with ChangeNotifier {
List<Expense> _expenses = [];
double _totalExpenses = 0.0;
final DatabaseHelper _dbHelper = DatabaseHelper();
List<Expense> get expenses => _expenses;
double get totalExpenses => _totalExpenses;
ExpenseProvider() {
loadExpenses(); // 初始加载
}
Future<void> loadExpenses() async {
_expenses = await _dbHelper.getAllExpenses();
_totalExpenses = await _dbHelper.getTotalExpenses();
notifyListeners(); // 通知 UI 重建
}
Future<void> addExpense(Expense expense) async {
await _dbHelper.insertExpense(expense);
await loadExpenses(); // 重新加载数据并通知
}
Future<void> updateExpense(Expense expense) async {
await _dbHelper.updateExpense(expense);
await loadExpenses(); // 重新加载数据并通知
}
Future<void> deleteExpense(int id) async {
await _dbHelper.deleteExpense(id);
await loadExpenses(); // 重新加载数据并通知
}
}

设置根 widget 并提供 ExpenseProvider。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'expense_provider.dart';
import 'screens/expense_list_screen.dart'; // 下一步创建此屏幕
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 向 widget 树提供 ExpenseProvider
return ChangeNotifierProvider(
create: (context) => ExpenseProvider(),
child: MaterialApp(
title: 'Expense Tracker',
theme: ThemeData(
primarySwatch: Colors.purple,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ExpenseListScreen(), // 设置主屏幕
),
);
}
}

5. 支出列表屏幕(lib/screens/expense_list_screen.dart)

Section titled “5. 支出列表屏幕(lib/screens/expense_list_screen.dart)”

显示支出列表和总金额。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../expense_provider.dart';
import '../expense.dart';
import 'expense_form_screen.dart'; // 下一步创建此屏幕
import 'package:intl/intl.dart';
class ExpenseListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 访问 provider
final expenseProvider = Provider.of<ExpenseProvider>(context);
final expenses = expenseProvider.expenses;
final total = expenseProvider.totalExpenses;
final currencyFormat = NumberFormat.currency(locale: 'en_US', symbol: '\$');
return Scaffold(
appBar: AppBar(
title: Text('Expense Tracker (${currencyFormat.format(total)})'),
),
body: expenses.isEmpty
? Center(child: Text('No expenses yet. Add some!'))
: ListView.builder(
itemCount: expenses.length,
itemBuilder: (ctx, index) {
final expense = expenses[index];
return Dismissible(
key: ValueKey(expense.id), // 用于滑动的唯一 key
background: Container(
color: Theme.of(context).colorScheme.error,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
margin: EdgeInsets.symmetric(horizontal: 15, vertical: 4),
child: Icon(Icons.delete, color: Colors.white, size: 40),
),
direction: DismissDirection.endToStart,
confirmDismiss: (direction) {
return showDialog( // 请求确认
context: context,
builder: (ctx) => AlertDialog(
title: Text('Are you sure?'),
content: Text('Do you want to remove this expense?'),
actions: <Widget>[
TextButton(child: Text('No'), onPressed: () => Navigator.of(ctx).pop(false)),
TextButton(child: Text('Yes'), onPressed: () => Navigator.of(ctx).pop(true)),
],
),
);
},
onDismissed: (direction) {
if (expense.id != null) {
Provider.of<ExpenseProvider>(context, listen: false)
.deleteExpense(expense.id!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${expense.category} expense deleted.')),
);
}
},
child: Card(
margin: EdgeInsets.symmetric(horizontal: 15, vertical: 4),
child: ListTile(
leading: CircleAvatar(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: FittedBox(child: Text(currencyFormat.format(expense.amount))),
),
),
title: Text(expense.category,
style: Theme.of(context).textTheme.titleMedium),
subtitle: Text(expense.formattedDate),
onTap: () { // 导航到编辑屏幕
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ExpenseFormScreen(expense: expense),
),
);
},
),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
// 传递 null 表示添加新支出
builder: (context) => ExpenseFormScreen(expense: null),
),
);
},\n ),
);
}
}

6. 支出表单屏幕(lib/screens/expense_form_screen.dart)

Section titled “6. 支出表单屏幕(lib/screens/expense_form_screen.dart)”

一个 StatefulWidget,用于处理用户添加/编辑支出的输入。

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../expense.dart';
import '../expense_provider.dart';
class ExpenseFormScreen extends StatefulWidget {
final Expense? expense; // 添加时为 null,编辑时非 null
ExpenseFormScreen({this.expense});
@override
_ExpenseFormScreenState createState() => _ExpenseFormScreenState();
}
class _ExpenseFormScreenState extends State<ExpenseFormScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _amountController;
late TextEditingController _categoryController;
late DateTime _selectedDate;
bool _isEditing = false;
@override
void initState() {
super.initState();
_isEditing = widget.expense != null;
_amountController = TextEditingController(
text: _isEditing ? widget.expense!.amount.toString() : '');
_categoryController = TextEditingController(
text: _isEditing ? widget.expense!.category : '');
_selectedDate = _isEditing ? widget.expense!.date : DateTime.now();
}
@override
void dispose() {
_amountController.dispose();
_categoryController.dispose();
super.dispose();
}
Future<void> _presentDatePicker() async {
final pickedDate = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020), // 根据需要调整范围
lastDate: DateTime.now(),
);
if (pickedDate != null && pickedDate != _selectedDate) {
setState(() {
_selectedDate = pickedDate;
});
}
}
void _submitData() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save(); // 为每个字段调用 onSaved
final enteredAmount = double.tryParse(_amountController.text);
final enteredCategory = _categoryController.text;
if (enteredAmount == null || enteredAmount <= 0 || enteredCategory.isEmpty) {
// 基本验证检查(validator 已覆盖,但这是一个好的实践)
return;
}
final expenseProvider = Provider.of<ExpenseProvider>(context, listen: false);
final newOrUpdatedExpense = Expense(
id: _isEditing ? widget.expense!.id : null, // 编辑时保留 id
amount: enteredAmount,
date: _selectedDate,
category: enteredCategory,
);
if (_isEditing) {
expenseProvider.updateExpense(newOrUpdatedExpense);
} else {
expenseProvider.addExpense(newOrUpdatedExpense);
}
Navigator.of(context).pop(); // 保存后返回
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isEditing ? 'Edit Expense' : 'Add Expense'),
actions: [
IconButton(icon: Icon(Icons.save), onPressed: _submitData),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: _amountController,
decoration: InputDecoration(labelText: 'Amount'),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an amount.';
}
if (double.tryParse(value) == null || double.parse(value) <= 0) {
return 'Please enter a valid positive amount.';
}
return null;
},
),
TextFormField(
controller: _categoryController,
decoration: InputDecoration(labelText: 'Category'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a category.';
}
return null;
},
),
SizedBox(height: 20),
Row(
children: <Widget>[
Expanded(
child: Text(
'Date: ${DateFormat('yyyy-MM-dd').format(_selectedDate)}',
),
),
TextButton(
onPressed: _presentDatePicker,
child: Text('Choose Date'),
),
],
),
SizedBox(height: 30),
ElevatedButton(
child: Text(_isEditing ? 'Update Expense' : 'Add Expense'),
onPressed: _submitData,
),
],
),
),
),
);
}
}

运行应用(flutter run)。您现在应该能够:

  • 查看初始支出列表(如果添加了样本数据)。
  • 在 AppBar 中查看总支出。
  • 点击 ’+’ 按钮导航到表单并添加新支出。
  • 点击列表中的现有支出以导航到表单并编辑它。
  • 从右向左滑动支出以删除它(需要确认)。
  • 添加、编辑或删除后,列表和总金额应自动更新。

此示例演示了如何结合数据建模、数据库交互、状态管理、导航和 UI 构建来创建一个功能完整的 Flutter 应用。您可以通过添加过滤、图表或云同步等功能来进一步扩展它。