Flutter-고급 애플리케이션 작성
이 장에서는 본격적인 모바일 응용 프로그램 인 cost_calculator를 작성하는 방법을 배웁니다. cost_calculator의 목적은 비용 정보를 저장하는 것입니다. 응용 프로그램의 전체 기능은 다음과 같습니다-
비용 목록.
새 비용을 입력하는 양식.
기존 비용을 편집 / 삭제하는 옵션.
어떤 경우에도 총 비용.
우리는 Flutter 프레임 워크의 아래에 언급 된 고급 기능을 사용하여 cost_calculator 애플리케이션을 프로그래밍 할 것입니다.
비용 목록을 표시하기 위해 ListView의 고급 사용.
양식 프로그래밍.
비용을 저장하는 SQLite 데이터베이스 프로그래밍.
프로그래밍을 단순화하기위한 scoped_model 상태 관리.
프로그래밍을 시작하겠습니다. expense_calculator 신청.
Android 스튜디오에서 새로운 Flutter 애플리케이션 (expert_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 스튜디오는 pubspec.yaml이 업데이트되었다는 다음 경고를 표시합니다.

종속성 가져 오기 옵션을 클릭하십시오. Android 스튜디오는 인터넷에서 패키지를 가져와 애플리케이션에 맞게 적절하게 구성합니다.
main.dart에서 기존 코드를 제거하십시오.
새 파일 인 Expense.dart를 추가하여 비용 클래스를 작성하십시오. 비용 클래스에는 다음과 같은 속성과 메서드가 있습니다.
property: id − SQLite 데이터베이스에서 비용 항목을 나타내는 고유 ID.
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 메소드를 사용하여 데이터베이스에서 사용 가능한 비용 ID를 기반으로 특정 비용 정보를 가져옵니다. 사용자에게 특정 비용 정보를 표시하는 데 사용됩니다.
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을 만듭니다. 이 모델의 목적은 사용자 비용의 전체 정보를 메모리에 저장하고 사용자의 비용이 메모리에서 변경 될 때마다 애플리케이션의 사용자 인터페이스를 업데이트하는 것입니다. scoped_model 패키지의 모델 클래스를 기반으로합니다. 그것은 다음과 같은 속성과 방법을 가지고 있습니다-
_items-비용의 개인 목록.
items-예상치 못한 또는 우발적 인 목록 변경을 방지하기 위해 UnmodifiableListView <Expense>로 _items에 대한 getter.
totalExpense-항목 변수를 기준으로 총 비용에 대한 게터.
double get totalExpense {
double amount = 0.0;
for(var i = 0; i < _items.length; i++) {
amount = amount + _items[i].amount;
}
return amount;
}
load-데이터베이스에서 _items 변수로 전체 비용을로드하는 데 사용됩니다. 또한 UI를 업데이트하기 위해 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 변수와 데이터베이스에 추가하는 데 사용됩니다. 또한 UI를 업데이트하기 위해 notifyListeners를 호출합니다.
void add(Expense item) {
SQLiteDbProvider.db.insert(item).then((val) {
_items.add(val); notifyListeners();
});
}
업데이트-비용 항목을 _items 변수와 데이터베이스로 업데이트하는 데 사용됩니다. 또한 UI를 업데이트하기 위해 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 변수에있는 기존 비용 항목을 제거하는 데 사용됩니다. 또한 UI를 업데이트하기 위해 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';
주 함수를 추가하고 ScopedModel <ExpenseListModel> 위젯을 전달하여 runApp을 호출합니다.
void main() {
final expenses = ExpenseListModel();
runApp(
ScopedModel<ExpenseListModel>(model: expenses, child: MyApp(),)
);
}
Here,
비용 개체는 데이터베이스에서 모든 사용자 비용 정보를로드합니다. 또한 응용 프로그램을 처음 열 때 적절한 테이블을 사용하여 필요한 데이터베이스를 만듭니다.
ScopedModel은 애플리케이션의 전체 수명주기 동안 비용 정보를 제공하고 모든 인스턴스에서 애플리케이션의 상태를 유지합니다. 이를 통해 StatefulWidget 대신 StatelessWidget을 사용할 수 있습니다.
MaterialApp 위젯을 사용하여 간단한 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 위젯을 생성하여 상단에 총 비용과 함께 모든 사용자의 비용 정보를 표시합니다. 오른쪽 하단의 플로팅 버튼은 새로운 비용을 추가하는 데 사용됩니다.
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의 validator 속성은 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'),
),
],
),
),
),
);
}
}
이제 애플리케이션을 실행하십시오.
플로팅 버튼을 사용하여 새로운 비용을 추가하십시오.
비용 항목을 탭하여 기존 비용을 편집합니다.
비용 항목을 양방향으로 스 와이프하여 기존 비용을 삭제합니다.
응용 프로그램의 일부 스크린 샷은 다음과 같습니다-


