flutter_building_complete_application
Flutter - 构建一个完整的应用
Section titled “Flutter - 构建一个完整的应用”在本章中,我们将整合之前学习到的许多概念,构建一个更完整的应用:一个记账应用(Expense Tracker)。这个应用将允许用户记录支出、查看支出列表、查看总支出金额,并可能编辑/删除条目。
我们将使用:
- 核心 Flutter Widget(
Scaffold、ListView、ListTile、FloatingActionButton、Form、TextFormField)。 sqflite用于本地数据持久化。provider用于状态管理。- 使用
intl包进行日期格式化。 - 基本的表单验证。
-
创建一个新的 Flutter 项目:
flutter create expense_tracker_app -
添加依赖到
pubspec.yaml(使用最新版本): -
运行
flutter pub get。
1. 数据模型(lib/expense.dart)
Section titled “1. 数据模型(lib/expense.dart)”定义 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(); // 重新加载数据并通知 }}4. 主应用设置(lib/main.dart)
Section titled “4. 主应用设置(lib/main.dart)”设置根 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 应用。您可以通过添加过滤、图表或云同步等功能来进一步扩展它。