Flutter - Escrevendo aplicativos avançados
Neste capítulo, vamos aprender como escrever um aplicativo móvel completo, expens_calculator. O objetivo do expens_calculator é armazenar nossas informações de despesas. O recurso completo do aplicativo é o seguinte -
Lista de despesas.
Formulário para entrada de novas despesas.
Opção para editar / excluir despesas existentes.
Despesas totais em qualquer instância.
Vamos programar o aplicativo Cost_calculator usando os recursos avançados do Flutter mencionados abaixo.
Uso avançado de ListView para mostrar a lista de despesas.
Programação de formulários.
Programação de banco de dados SQLite para armazenar nossas despesas.
gerenciamento de estado scoped_model para simplificar nossa programação.
Vamos começar a programar o expense_calculator inscrição.
Crie um novo aplicativo Flutter, Cost_calculator no Android Studio.
Abra pubspec.yaml e adicione dependências de pacote.
dependencies:
flutter:
sdk: flutter
sqflite: ^1.1.0
path_provider: ^0.5.0+1
scoped_model: ^1.0.1
intl: any
Observe esses pontos aqui -
sqflite é usado para programação de banco de dados SQLite.
path_provider é usado para obter o caminho do aplicativo específico do sistema.
scoped_model é usado para gerenciamento de estado.
intl é usado para formatação de data.
O Android Studio exibirá o seguinte alerta de que o pubspec.yaml está atualizado.
Clique na opção Obter dependências. O Android Studio obterá o pacote da Internet e o configurará corretamente para o aplicativo.
Remova o código existente em main.dart.
Adicione um novo arquivo, Expense.dart para criar a classe de despesas. A classe de despesas terá as propriedades e métodos abaixo.
property: id - ID único para representar uma entrada de despesas no banco de dados SQLite.
property: amount - Valor gasto.
property: date - Data em que o valor é gasto.
property: category- Categoria representa a área em que o valor é gasto. por exemplo, comida, viagens, etc.,
formattedDate - Usado para formatar a propriedade de data
fromMap - Usado para mapear o campo da tabela do banco de dados para a propriedade no objeto de despesa e para criar um novo objeto de despesa.
factory Expense.fromMap(Map<String, dynamic> data) {
return Expense(
data['id'],
data['amount'],
DateTime.parse(data['date']),
data['category']
);
}
toMap - Usado para converter o objeto de despesa em Dart Map, que pode ser usado posteriormente na programação de banco de dados
Map<String, dynamic> toMap() => {
"id" : id,
"amount" : amount,
"date" : date.toString(),
"category" : category,
};
columns - Variável estática usada para representar o campo do banco de dados.
Insira e salve o código a seguir no arquivo 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,
};
}
O código acima é simples e autoexplicativo.
Adicione um novo arquivo, Database.dart, para criar a classe SQLiteDbProvider. O objetivo da classe SQLiteDbProvider é o seguinte -
Obtenha todas as despesas disponíveis no banco de dados usando o método getAllExpenses. Ele será usado para listar todas as informações de despesas do usuário.
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;
}
Obtenha informações de despesas específicas com base na identidade de despesas disponível no banco de dados usando o método getExpenseById. Ele será usado para mostrar as informações de despesas específicas ao usuário.
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;
}
Obtenha as despesas totais do usuário usando o método getTotalExpense. Ele será usado para mostrar a despesa total atual para o usuário.
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;
}
Adicione novas informações de despesas no banco de dados usando o método de inserção. Ele será usado para adicionar uma nova entrada de despesas ao aplicativo pelo usuário.
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);
}
Atualize as informações de despesas existentes usando o método de atualização. Ele será usado para editar e atualizar a entrada de despesas existente disponível no sistema pelo usuário.
update(Expense product) async {
final db = await database;
var result = await db.update("Expense", product.toMap(),
where: "id = ?", whereArgs: [product.id]);
return result;
}
Exclua as informações de despesas existentes usando o método de exclusão. Será utilizado para remover a entrada de despesas existente disponível no sistema pelo usuário.
delete(int id) async {
final db = await database;
db.delete("Expense", where: "id = ?", whereArgs: [id]);
}
O código completo da classe SQLiteDbProvider é o seguinte -
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 é a propriedade para obter o objeto SQLiteDbProvider.
initDB é um método usado para selecionar e abrir o banco de dados SQLite.
Crie um novo arquivo, ExpenseListModel.dart para criar ExpenseListModel. O objetivo do modelo é manter a informação completa dos gastos do usuário na memória e atualizar a interface do usuário do aplicativo sempre que os gastos do usuário mudarem na memória. É baseado na classe Model do pacote scoped_model. Possui as seguintes propriedades e métodos -
_items - lista privada de despesas.
items - getter para _items as UnmodifiableListView <Expense> para prevenir mudanças inesperadas ou acidentais na lista.
totalExpense - getter para despesas totais com base na variável de itens.
double get totalExpense {
double amount = 0.0;
for(var i = 0; i < _items.length; i++) {
amount = amount + _items[i].amount;
}
return amount;
}
load - usado para carregar as despesas completas do banco de dados e na variável _items. Ele também chama NoticeListeners para atualizar a IU.
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 - usado para obter uma despesa particular da variável _items.
Expense byId(int id) {
for(var i = 0; i < _items.length; i++) {
if(_items[i].id == id) {
return _items[i];
}
}
return null;
}
add - Usado para adicionar um novo item de despesa na variável _items, bem como no banco de dados. Ele também chama NoticeListeners para atualizar a IU.
void add(Expense item) {
SQLiteDbProvider.db.insert(item).then((val) {
_items.add(val); notifyListeners();
});
}
Atualizar - usado para atualizar o item de despesa na variável _items, bem como no banco de dados. Ele também chama NoticeListeners para atualizar a IU.
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 - Usado para remover um item de despesa existente na variável _items, bem como do banco de dados. Ele também chama NoticeListeners para atualizar a IU.
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();
}
O código completo da classe ExpenseListModel é o seguinte -
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();
}
}
Abra o arquivo main.dart. Importe as classes conforme especificado abaixo -
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'ExpenseListModel.dart';
import 'Expense.dart';
Adicione a função principal e chame runApp passando o widget ScopedModel <ExpenseListModel>.
void main() {
final expenses = ExpenseListModel();
runApp(
ScopedModel<ExpenseListModel>(model: expenses, child: MyApp(),)
);
}
Here,
O objeto de despesas carrega todas as informações de despesas do usuário do banco de dados. Além disso, quando o aplicativo é aberto pela primeira vez, ele criará o banco de dados necessário com as tabelas adequadas.
ScopedModel fornece as informações de gastos durante todo o ciclo de vida da aplicação e garante a manutenção do estado da aplicação em qualquer instância. Ele nos permite usar StatelessWidget em vez de StatefulWidget.
Crie um MyApp simples usando o widget 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'),
);
}
}
Crie o widget MyHomePage para exibir todas as informações de despesas do usuário junto com as despesas totais na parte superior. O botão flutuante no canto inferior direito será usado para adicionar novas despesas.
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 é usado para passar o modelo de despesas para o widget ListView e FloatingActionButton.
Os widgets ListView.separated e ListTile são usados para listar as informações de despesas.
O widget descartável é usado para excluir a entrada de despesas usando o gesto de deslizar.
O Navigator é usado para abrir a interface de edição de uma entrada de despesa. Ele pode ser ativado tocando em uma entrada de despesa.
Crie um widget FormPage. O objetivo do widget FormPage é adicionar ou atualizar uma entrada de despesas. Ele lida com a validação da entrada de despesas também.
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 é usado para criar entrada de formulário.
A propriedade validator de TextFormField é usada para validar o elemento do formulário junto com os padrões RegEx.
A função _submit é usada junto com o objeto de despesas para adicionar ou atualizar as despesas no banco de dados.
O código completo do arquivo main.dart é o seguinte -
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'),
),
],
),
),
),
);
}
}
Agora, execute o aplicativo.
Adicione novas despesas usando o botão flutuante.
Edite despesas existentes tocando na entrada de despesas.
Exclua as despesas existentes passando a entrada de despesas em qualquer direção.
Algumas das capturas de tela do aplicativo são as seguintes -