Flutter - Написание сложных приложений
В этой главе мы узнаем, как написать полноценное мобильное приложение cost_calculator. Назначение параметра cost_calculator - хранить информацию о наших расходах. Полная функция приложения выглядит следующим образом -
Список расходов.
Форма для ввода новых расходов.
Возможность редактировать / удалять существующие расходы.
Общие расходы в любом случае.
Мы собираемся запрограммировать приложение cost_calculator, используя указанные ниже расширенные функции фреймворка Flutter.
Расширенное использование ListView для отображения списка расходов.
Программирование форм.
Программирование базы данных SQLite для хранения наших расходов.
scoped_model управление состоянием для упрощения программирования.
Приступим к программированию expense_calculator применение.
Создайте новое приложение Flutter, cost_calculator, в студии Android.
Откройте 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 отобразит следующее предупреждение об обновлении pubspec.yaml.
Щелкните параметр Получить зависимости. Студия Android получит пакет из Интернета и правильно настроит его для приложения.
Удалите существующий код в main.dart.
Добавьте новый файл Expense.dart, чтобы создать класс Expense. Класс расходов будет иметь следующие свойства и методы.
property: id - Уникальный идентификатор для представления записи о расходах в базе данных SQLite.
property: amount - потраченная сумма.
property: date - Дата, когда сумма будет потрачена.
property: category- Категория представляет собой область, в которой расходуется сумма. например, еда, путешествия и т. д.,
formattedDate - Используется для форматирования свойства даты
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;
}
Добавьте новую информацию о расходах в базу данных с помощью метода вставки. Он будет использоваться для добавления пользователем новой записи расхода в приложение.
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]);
}
Полный код класса 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]);
}
}
Here,
database - это свойство для получения объекта SQLiteDbProvider.
initDB - это метод, используемый для выбора и открытия базы данных SQLite.
Создайте новый файл ExpenseListModel.dart для создания ExpenseListModel. Цель модели - хранить полную информацию о расходах пользователя в памяти и обновлять пользовательский интерфейс приложения всякий раз, когда расходы пользователя изменяются в памяти. Он основан на классе Model из пакета scoped_model. Он имеет следующие свойства и методы -
_items - личный список расходов.
items - геттер для _items как UnmodifiableListView <Expense>, чтобы предотвратить неожиданные или случайные изменения в списке.
totalExpense - метод получения общих расходов на основе переменной items.
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 для обновления пользовательского интерфейса.
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 для обновления пользовательского интерфейса.
void add(Expense item) {
SQLiteDbProvider.db.insert(item).then((val) {
_items.add(val); notifyListeners();
});
}
Обновление - используется для обновления статьи расходов в переменной _items, а также в базе данных. Он также вызывает 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();
}
delete - используется для удаления существующей статьи расходов в переменной _items, а также из базы данных. Он также вызывает 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();
}
Полный код класса 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';
Добавьте основную функцию и вызовите runApp, передав виджет ScopedModel <ExpenseListModel>.
void main() {
final expenses = ExpenseListModel();
runApp(
ScopedModel<ExpenseListModel>(model: expenses, child: MyApp(),)
);
}
Here,
Объект затрат загружает всю информацию о расходах пользователя из базы данных. Кроме того, когда приложение открывается в первый раз, оно создает необходимую базу данных с соответствующими таблицами.
ScopedModel предоставляет информацию о расходах в течение всего жизненного цикла приложения и обеспечивает поддержание состояния приложения в любой момент. Это позволяет нам использовать StatelessWidget вместо StatefulWidget.
Создайте простое приложение MyApp с помощью виджета MaterialApp.
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, чтобы отображать всю информацию о расходах пользователя вместе с общими расходами вверху. Плавающая кнопка в правом нижнем углу будет использоваться для добавления новых расходов.
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), );
}
)
);
}
}
Here,
ScopedModelDescendant используется для передачи модели расходов в виджет ListView и FloatingActionButton.
ListView.separated и виджет ListTile используются для вывода информации о расходах.
Скрываемый виджет используется для удаления записи о расходах с помощью жеста смахивания.
Навигатор используется для открытия интерфейса редактирования записи расхода. Его можно активировать, коснувшись записи расхода.
Создайте виджет FormPage. Назначение виджета FormPage - добавить или обновить запись о расходах. Он также выполняет проверку записей о расходах.
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'),
),
],
),
),
),
);
}
}
Here,
TextFormField используется для создания записи в форме.
Свойство валидатора TextFormField используется для проверки элемента формы вместе с шаблонами RegEx.
Функция _submit используется вместе с объектом расходов для добавления или обновления расходов в базу данных.
Полный код файла 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'),
),
],
),
),
),
);
}
}
Теперь запустите приложение.
Добавьте новые расходы с помощью плавающей кнопки.
Отредактируйте существующие расходы, коснувшись записи расхода.
Удалите существующие расходы, проведя по записи расхода в любом направлении.
Некоторые из снимков экрана приложения следующие -