- Flutter 教程
- Flutter - 首页
- Flutter - 简介
- Flutter - 安装
- 在 Android Studio 中创建简单的应用
- Flutter - 应用架构
- Dart 编程入门
- Flutter - Widget 入门
- Flutter - 布局入门
- Flutter - 手势入门
- Flutter - 状态管理
- Flutter - 动画
- Flutter - 编写 Android 特定代码
- Flutter - 编写 iOS 特定代码
- Flutter - 包入门
- Flutter - 访问 REST API
- Flutter - 数据库概念
- Flutter - 国际化
- Flutter - 测试
- Flutter - 部署
- Flutter - 开发工具
- Flutter - 开发高级应用
- Flutter - 总结
- Flutter 有用资源
- Flutter - 快速指南
- Flutter - 有用资源
- Flutter - 讨论
Flutter - 开发高级应用
在本章中,我们将学习如何编写一个完整的移动应用程序,expense_calculator。expense_calculator 的目的是存储我们的支出信息。应用程序的完整功能如下:
支出列表。
输入新支出的表单。
编辑/删除现有支出的选项。
任何时候的总支出。
我们将使用 Flutter 框架下面提到的高级功能来编写 expense_calculator 应用程序。
高级 ListView 用法来显示支出列表。
表单编程。
SQLite 数据库编程来存储我们的支出。
scoped_model 状态管理来简化我们的编程。
让我们开始编写expense_calculator应用程序。
在 Android Studio 中创建一个新的 Flutter 应用程序,expense_calculator。
打开 pubspec.yaml 并添加包依赖项。
dependencies: flutter: sdk: flutter sqflite: ^1.1.0 path_provider: ^0.5.0+1 scoped_model: ^1.0.1 intl: any
请注意以下几点:
sqflite 用于 SQLite 数据库编程。
path_provider 用于获取特定于系统的应用程序路径。
scoped_model 用于状态管理。
intl 用于日期格式化。
Android Studio 将显示以下警报,提示 pubspec.yaml 已更新。
点击“获取依赖项”选项。Android Studio 将从互联网获取包并为应用程序正确配置它。
删除 main.dart 中的现有代码。
添加新文件 Expense.dart 以创建 Expense 类。Expense 类将具有以下属性和方法。
属性:id - 在 SQLite 数据库中表示支出条目的唯一 ID。
属性:amount - 支出的金额。
属性:date - 支出日期。
属性:category - 类别表示支出领域,例如食物、旅行等。
formattedDate - 用于格式化 date 属性
fromMap - 用于将数据库表中的字段映射到支出对象中的属性,并创建新的支出对象。
factory Expense.fromMap(Map<String, dynamic> data) { return Expense( data['id'], data['amount'], DateTime.parse(data['date']), data['category'] ); }
toMap - 用于将支出对象转换为 Dart Map,这可以进一步用于数据库编程
Map<String, dynamic> toMap() => { "id" : id, "amount" : amount, "date" : date.toString(), "category" : category, };
columns - 静态变量,用于表示数据库字段。
将以下代码输入 Expense.dart 文件并保存。
import 'package:intl/intl.dart'; class Expense { final int id; final double amount; final DateTime date; final String category; String get formattedDate { var formatter = new DateFormat('yyyy-MM-dd'); return formatter.format(this.date); } static final columns = ['id', 'amount', 'date', 'category']; Expense(this.id, this.amount, this.date, this.category); factory Expense.fromMap(Map<String, dynamic> data) { return Expense( data['id'], data['amount'], DateTime.parse(data['date']), data['category'] ); } Map<String, dynamic> toMap() => { "id" : id, "amount" : amount, "date" : date.toString(), "category" : category, }; }
以上代码简单易懂。
添加新文件 Database.dart 以创建 SQLiteDbProvider 类。SQLiteDbProvider 类的目的是:
使用 getAllExpenses 方法获取数据库中所有可用的支出。它将用于列出所有用户的支出信息。
Future<List<Expense>> getAllExpenses() async { final db = await database; List<Map> results = await db.query( "Expense", columns: Expense.columns, orderBy: "date DESC" ); List<Expense> expenses = new List(); results.forEach((result) { Expense expense = Expense.fromMap(result); expenses.add(expense); }); return expenses; }
使用 getExpenseById 方法根据数据库中可用的支出标识获取特定支出信息。它将用于向用户显示特定的支出信息。
Future<Expense> getExpenseById(int id) async { final db = await database; var result = await db.query("Expense", where: "id = ", whereArgs: [id]); return result.isNotEmpty ? Expense.fromMap(result.first) : Null; }
使用 getTotalExpense 方法获取用户的总支出。它将用于向用户显示当前的总支出。
Future<double> getTotalExpense() async { final db = await database; List<Map> list = await db.rawQuery( "Select SUM(amount) as amount from expense" ); return list.isNotEmpty ? list[0]["amount"] : Null; }
使用 insert 方法将新的支出信息添加到数据库中。它将用于用户在应用程序中添加新的支出条目。
Future<Expense> insert(Expense expense) async { final db = await database; var maxIdResult = await db.rawQuery( "SELECT MAX(id)+1 as last_inserted_id FROM Expense" ); var id = maxIdResult.first["last_inserted_id"]; var result = await db.rawInsert( "INSERT Into Expense (id, amount, date, category)" " VALUES (?, ?, ?, ?)", [ id, expense.amount, expense.date.toString(), expense.category ] ); return Expense(id, expense.amount, expense.date, expense.category); }
使用 update 方法更新现有支出信息。它将用于用户编辑和更新系统中可用的现有支出条目。
update(Expense product) async { final db = await database; var result = await db.update("Expense", product.toMap(), where: "id = ?", whereArgs: [product.id]); return result; }
使用 delete 方法删除现有支出信息。它将用于用户删除系统中可用的现有支出条目。
delete(int id) async { final db = await database; db.delete("Expense", where: "id = ?", whereArgs: [id]); }
SQLiteDbProvider 类的完整代码如下:
import 'dart:async'; import 'dart:io'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; import 'Expense.dart'; class SQLiteDbProvider { SQLiteDbProvider._(); static final SQLiteDbProvider db = SQLiteDbProvider._(); static Database _database; Future<Database> get database async { if (_database != null) return _database; _database = await initDB(); return _database; } initDB() async { Directory documentsDirectory = await getApplicationDocumentsDirectory(); String path = join(documentsDirectory.path, "ExpenseDB2.db"); return await openDatabase( path, version: 1, onOpen:(db){}, onCreate: (Database db, int version) async { await db.execute( "CREATE TABLE Expense ( ""id INTEGER PRIMARY KEY," "amount REAL," "date TEXT," "category TEXT"" ) "); await db.execute( "INSERT INTO Expense ('id', 'amount', 'date', 'category') values (?, ?, ?, ?)",[1, 1000, '2019-04-01 10:00:00', "Food"] ); /*await db.execute( "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') values (?, ?, ?, ?, ?)", [ 2, "Pixel", "Pixel is the most feature phone ever", 800, "pixel.png" ] ); await db.execute( "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') values (?, ?, ?, ?, ?)", [ 3, "Laptop", "Laptop is most productive development tool", 2000, "laptop.png" ] ); await db.execute( "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') values (?, ?, ?, ?, ?)", [ 4, "Tablet", "Laptop is most productive development tool", 1500, "tablet.png" ] ); await db.execute( "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') values (?, ?, ?, ?, ?)", [ 5, "Pendrive", "iPhone is the stylist phone ever", 100, "pendrive.png" ] ); await db.execute( "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') values (?, ?, ?, ?, ?)", [ 6, "Floppy Drive", "iPhone is the stylist phone ever", 20, "floppy.png" ] ); */ } ); } Future<List<Expense>> getAllExpenses() async { final db = await database; List<Map> results = await db.query( "Expense", columns: Expense.columns, orderBy: "date DESC" ); List<Expense> expenses = new List(); results.forEach((result) { Expense expense = Expense.fromMap(result); expenses.add(expense); }); return expenses; } Future<Expense> getExpenseById(int id) async { final db = await database; var result = await db.query("Expense", where: "id = ", whereArgs: [id]); return result.isNotEmpty ? Expense.fromMap(result.first) : Null; } Future<double> getTotalExpense() async { final db = await database; List<Map> list = await db.rawQuery( "Select SUM(amount) as amount from expense" ); return list.isNotEmpty ? list[0]["amount"] : Null; } Future<Expense> insert(Expense expense) async { final db = await database; var maxIdResult = await db.rawQuery( "SELECT MAX(id)+1 as last_inserted_id FROM Expense" ); var id = maxIdResult.first["last_inserted_id"]; var result = await db.rawInsert( "INSERT Into Expense (id, amount, date, category)" " VALUES (?, ?, ?, ?)", [ id, expense.amount, expense.date.toString(), expense.category ] ); return Expense(id, expense.amount, expense.date, expense.category); } update(Expense product) async { final db = await database; var result = await db.update( "Expense", product.toMap(), where: "id = ?", whereArgs: [product.id] ); return result; } delete(int id) async { final db = await database; db.delete("Expense", where: "id = ?", whereArgs: [id]); } }
这里,
database 是获取 SQLiteDbProvider 对象的属性。
initDB 是一个用于选择和打开 SQLite 数据库的方法。
创建一个新文件 ExpenseListModel.dart 以创建 ExpenseListModel。该模型的目的是在内存中保存用户支出的完整信息,并在用户内存中的支出发生变化时更新应用程序的用户界面。它基于 scoped_model 包中的 Model 类。它具有以下属性和方法:
_items - 支出的私有列表。
items - 作为 UnmodifiableListView<Expense> 的 _items 的 getter,以防止对列表进行意外或意外更改。
totalExpense - 基于 items 变量的总支出的 getter。
double get totalExpense { double amount = 0.0; for(var i = 0; i < _items.length; i++) { amount = amount + _items[i].amount; } return amount; }
load - 用于从数据库加载完整支出并将其加载到 _items 变量中。它还会调用 notifyListeners 以更新 UI。
void load() { Future<List<Expense>> list = SQLiteDbProvider.db.getAllExpenses(); list.then( (dbItems) { for(var i = 0; i < dbItems.length; i++) { _items.add(dbItems[i]); } notifyListeners(); }); }
byId - 用于从 _items 变量获取特定支出。
Expense byId(int id) { for(var i = 0; i < _items.length; i++) { if(_items[i].id == id) { return _items[i]; } } return null; }
add - 用于将新的支出项添加到 _items 变量以及数据库中。它还会调用 notifyListeners 以更新 UI。
void add(Expense item) { SQLiteDbProvider.db.insert(item).then((val) { _items.add(val); notifyListeners(); }); }
Update - 用于将支出项更新到 _items 变量以及数据库中。它还会调用 notifyListeners 以更新 UI。
void update(Expense item) { bool found = false; for(var i = 0; i < _items.length; i++) { if(_items[i].id == item.id) { _items[i] = item; found = true; SQLiteDbProvider.db.update(item); break; } } if(found) notifyListeners(); }
delete - 用于从 _items 变量以及数据库中删除现有的支出项。它还会调用 notifyListeners 以更新 UI。
void delete(Expense item) { bool found = false; for(var i = 0; i < _items.length; i++) { if(_items[i].id == item.id) { found = true; SQLiteDbProvider.db.delete(item.id); _items.removeAt(i); break; } } if(found) notifyListeners(); }
ExpenseListModel 类的完整代码如下:
import 'dart:collection'; import 'package:scoped_model/scoped_model.dart'; import 'Expense.dart'; import 'Database.dart'; class ExpenseListModel extends Model { ExpenseListModel() { this.load(); } final List<Expense> _items = []; UnmodifiableListView<Expense> get items => UnmodifiableListView(_items); /*Future<double> get totalExpense { return SQLiteDbProvider.db.getTotalExpense(); }*/ double get totalExpense { double amount = 0.0; for(var i = 0; i < _items.length; i++) { amount = amount + _items[i].amount; } return amount; } void load() { Future<List<Expense>> list = SQLiteDbProvider.db.getAllExpenses(); list.then( (dbItems) { for(var i = 0; i < dbItems.length; i++) { _items.add(dbItems[i]); } notifyListeners(); }); } Expense byId(int id) { for(var i = 0; i < _items.length; i++) { if(_items[i].id == id) { return _items[i]; } } return null; } void add(Expense item) { SQLiteDbProvider.db.insert(item).then((val) { _items.add(val); notifyListeners(); }); } void update(Expense item) { bool found = false; for(var i = 0; i < _items.length; i++) { if(_items[i].id == item.id) { _items[i] = item; found = true; SQLiteDbProvider.db.update(item); break; } } if(found) notifyListeners(); } void delete(Expense item) { bool found = false; for(var i = 0; i < _items.length; i++) { if(_items[i].id == item.id) { found = true; SQLiteDbProvider.db.delete(item.id); _items.removeAt(i); break; } } if(found) notifyListeners(); } }
打开 main.dart 文件。导入如下所示的类:
import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; import 'ExpenseListModel.dart'; import 'Expense.dart';
添加 main 函数并通过传递 ScopedModel<ExpenseListModel> widget 来调用 runApp。
void main() { final expenses = ExpenseListModel(); runApp( ScopedModel<ExpenseListModel>(model: expenses, child: MyApp(),) ); }
这里,
expenses 对象从数据库加载所有用户的支出信息。此外,当应用程序第一次打开时,它将创建具有正确表的所需数据库。
ScopedModel 在应用程序的整个生命周期中提供支出信息,并确保在任何时候都维护应用程序的状态。它使我们能够使用 StatelessWidget 而不是 StatefulWidget。
使用 MaterialApp widget 创建一个简单的 MyApp。
class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Expense', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Expense calculator'), ); } }
创建 MyHomePage widget 以显示所有用户的支出信息以及顶部的总支出。右下角的浮动按钮将用于添加新的支出。
class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: ScopedModelDescendant<ExpenseListModel>( builder: (context, child, expenses) { return ListView.separated( itemCount: expenses.items == null ? 1 : expenses.items.length + 1, itemBuilder: (context, index) { if (index == 0) { return ListTile( title: Text("Total expenses: " + expenses.totalExpense.toString(), style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),) ); } else { index = index - 1; return Dismissible( key: Key(expenses.items[index].id.toString()), onDismissed: (direction) { expenses.delete(expenses.items[index]); Scaffold.of(context).showSnackBar( SnackBar( content: Text( "Item with id, " + expenses.items[index].id.toString() + " is dismissed" ) ) ); }, child: ListTile( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => FormPage( id: expenses.items[index].id, expenses: expenses, ) ) ); }, leading: Icon(Icons.monetization_on), trailing: Icon(Icons.keyboard_arrow_right), title: Text(expenses.items[index].category + ": " + expenses.items[index].amount.toString() + " \nspent on " + expenses.items[index].formattedDate, style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic),)) ); } }, separatorBuilder: (context, index) { return Divider(); }, ); }, ), floatingActionButton: ScopedModelDescendant<ExpenseListModel>( builder: (context, child, expenses) { return FloatingActionButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ScopedModelDescendant<ExpenseListModel>( builder: (context, child, expenses) { return FormPage( id: 0, expenses: expenses, ); } ) ) ); // expenses.add(new Expense( // 2, 1000, DateTime.parse('2019-04-01 11:00:00'), 'Food') ); // print(expenses.items.length); }, tooltip: 'Increment', child: Icon(Icons.add), ); } ) ); } }
这里,
ScopedModelDescendant 用于将支出模型传递到 ListView 和 FloatingActionButton widget 中。
ListView.separated 和 ListTile widget 用于列出支出信息。
Dismissible widget 用于使用滑动手势删除支出条目。
Navigator 用于打开支出条目的编辑界面。可以通过点击支出条目来激活它。
创建一个 FormPage widget。FormPage widget 的目的是添加或更新支出条目。它也处理支出条目的验证。
class FormPage extends StatefulWidget { FormPage({Key key, this.id, this.expenses}) : super(key: key); final int id; final ExpenseListModel expenses; @override _FormPageState createState() => _FormPageState(id: id, expenses: expenses); } class _FormPageState extends State<FormPage> { _FormPageState({Key key, this.id, this.expenses}); final int id; final ExpenseListModel expenses; final scaffoldKey = GlobalKey<ScaffoldState>(); final formKey = GlobalKey<FormState>(); double _amount; DateTime _date; String _category; void _submit() { final form = formKey.currentState; if (form.validate()) { form.save(); if (this.id == 0) expenses.add(Expense(0, _amount, _date, _category)); else expenses.update(Expense(this.id, _amount, _date, _category)); Navigator.pop(context); } } @override Widget build(BuildContext context) { return Scaffold( key: scaffoldKey, appBar: AppBar( title: Text('Enter expense details'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: formKey, child: Column( children: [ TextFormField( style: TextStyle(fontSize: 22), decoration: const InputDecoration( icon: const Icon(Icons.monetization_on), labelText: 'Amount', labelStyle: TextStyle(fontSize: 18) ), validator: (val) { Pattern pattern = r'^[1-9]\d*(\.\d+)?$'; RegExp regex = new RegExp(pattern); if (!regex.hasMatch(val)) return 'Enter a valid number'; else return null; }, initialValue: id == 0 ? '' : expenses.byId(id).amount.toString(), onSaved: (val) => _amount = double.parse(val), ), TextFormField( style: TextStyle(fontSize: 22), decoration: const InputDecoration( icon: const Icon(Icons.calendar_today), hintText: 'Enter date', labelText: 'Date', labelStyle: TextStyle(fontSize: 18), ), validator: (val) { Pattern pattern = r'^((?:19|20)\d\d)[- /.] (0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$'; RegExp regex = new RegExp(pattern); if (!regex.hasMatch(val)) return 'Enter a valid date'; else return null; }, onSaved: (val) => _date = DateTime.parse(val), initialValue: id == 0 ? '' : expenses.byId(id).formattedDate, keyboardType: TextInputType.datetime, ), TextFormField( style: TextStyle(fontSize: 22), decoration: const InputDecoration( icon: const Icon(Icons.category), labelText: 'Category', labelStyle: TextStyle(fontSize: 18) ), onSaved: (val) => _category = val, initialValue: id == 0 ? '' : expenses.byId(id).category.toString(), ), RaisedButton( onPressed: _submit, child: new Text('Submit'), ), ], ), ), ), ); } }
这里,
TextFormField 用于创建表单条目。
TextFormField 的 validator 属性用于使用 RegEx 模式验证表单元素。
_submit 函数与 expenses 对象一起使用,将支出添加到数据库或更新数据库中的支出。
main.dart 文件的完整代码如下:
import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; import 'ExpenseListModel.dart'; import 'Expense.dart'; void main() { final expenses = ExpenseListModel(); runApp( ScopedModel<ExpenseListModel>( model: expenses, child: MyApp(), ) ); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Expense', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Expense calculator'), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: ScopedModelDescendant<ExpenseListModel>( builder: (context, child, expenses) { return ListView.separated( itemCount: expenses.items == null ? 1 : expenses.items.length + 1, itemBuilder: (context, index) { if (index == 0) { return ListTile( title: Text("Total expenses: " + expenses.totalExpense.toString(), style: TextStyle(fontSize: 24,fontWeight: FontWeight.bold),) ); } else { index = index - 1; return Dismissible( key: Key(expenses.items[index].id.toString()), onDismissed: (direction) { expenses.delete(expenses.items[index]); Scaffold.of(context).showSnackBar( SnackBar( content: Text( "Item with id, " + expenses.items[index].id.toString() + " is dismissed" ) ) ); }, child: ListTile( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => FormPage( id: expenses.items[index].id, expenses: expenses, ) )); }, leading: Icon(Icons.monetization_on), trailing: Icon(Icons.keyboard_arrow_right), title: Text(expenses.items[index].category + ": " + expenses.items[index].amount.toString() + " \nspent on " + expenses.items[index].formattedDate, style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic),)) ); } }, separatorBuilder: (context, index) { return Divider(); }, ); }, ), floatingActionButton: ScopedModelDescendant<ExpenseListModel>( builder: (context, child, expenses) { return FloatingActionButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ScopedModelDescendant<ExpenseListModel>( builder: (context, child, expenses) { return FormPage( id: 0, expenses: expenses, ); } ) ) ); // expenses.add( new Expense( // 2, 1000, DateTime.parse('2019-04-01 11:00:00'), 'Food' ) ); // print(expenses.items.length); }, tooltip: 'Increment', child: Icon(Icons.add), ); } ) ); } } class FormPage extends StatefulWidget { FormPage({Key key, this.id, this.expenses}) : super(key: key); final int id; final ExpenseListModel expenses; @override _FormPageState createState() => _FormPageState(id: id, expenses: expenses); } class _FormPageState extends State<FormPage> { _FormPageState({Key key, this.id, this.expenses}); final int id; final ExpenseListModel expenses; final scaffoldKey = GlobalKey<ScaffoldState>(); final formKey = GlobalKey<FormState>(); double _amount; DateTime _date; String _category; void _submit() { final form = formKey.currentState; if (form.validate()) { form.save(); if (this.id == 0) expenses.add(Expense(0, _amount, _date, _category)); else expenses.update(Expense(this.id, _amount, _date, _category)); Navigator.pop(context); } } @override Widget build(BuildContext context) { return Scaffold( key: scaffoldKey, appBar: AppBar( title: Text('Enter expense details'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: formKey, child: Column( children: [ TextFormField( style: TextStyle(fontSize: 22), decoration: const InputDecoration( icon: const Icon(Icons.monetization_on), labelText: 'Amount', labelStyle: TextStyle(fontSize: 18) ), validator: (val) { Pattern pattern = r'^[1-9]\d*(\.\d+)?$'; RegExp regex = new RegExp(pattern); if (!regex.hasMatch(val)) return 'Enter a valid number'; else return null; }, initialValue: id == 0 ? '' : expenses.byId(id).amount.toString(), onSaved: (val) => _amount = double.parse(val), ), TextFormField( style: TextStyle(fontSize: 22), decoration: const InputDecoration( icon: const Icon(Icons.calendar_today), hintText: 'Enter date', labelText: 'Date', labelStyle: TextStyle(fontSize: 18), ), validator: (val) { Pattern pattern = r'^((?:19|20)\d\d)[- /.] (0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$'; RegExp regex = new RegExp(pattern); if (!regex.hasMatch(val)) return 'Enter a valid date'; else return null; }, onSaved: (val) => _date = DateTime.parse(val), initialValue: id == 0 ? '' : expenses.byId(id).formattedDate, keyboardType: TextInputType.datetime, ), TextFormField( style: TextStyle(fontSize: 22), decoration: const InputDecoration( icon: const Icon(Icons.category), labelText: 'Category', labelStyle: TextStyle(fontSize: 18) ), onSaved: (val) => _category = val, initialValue: id == 0 ? '' : expenses.byId(id).category.toString(), ), RaisedButton( onPressed: _submit, child: new Text('Submit'), ), ], ), ), ), ); } }
现在,运行应用程序。
使用浮动按钮添加新的支出。
通过点击支出条目来编辑现有支出。
通过向任一方向滑动支出条目来删除现有支出。
应用程序的一些屏幕截图如下: