- 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'),
),
],
),
),
),
);
}
}
现在,运行应用程序。
使用浮动按钮添加新的支出。
通过点击支出条目来编辑现有支出。
通过向任一方向滑动支出条目来删除现有支出。
应用程序的一些屏幕截图如下: