# 記帳程式
### step 1 程式框架建立
#### 檔案結構
```
lib
|-expenses.dart
|-main.dart
```
#### 程式碼構建
**main.dart**
```c=
import 'package:flutter/material.dart';
import 'package:expense_tracker/expenses.dart';
void main() {
runApp(
const MaterialApp(
home: ...,
),
);
}
```

在我們的介面上,我們有圖表(chart)跟訊息方塊(Expenses list),我們先在畫面上新增一個Scaffold,在其中加入一個Column,因為我們的圖表(chart)跟訊息方塊(Expenses list)是縱向排列的
expense.dart
```c=
import 'package:flutter/material.dart';
class Expenses extends StatefulWidget {
const Expenses({super.key});
@override
State<Expenses> createState() {
return _ExpensesState();
}
}
class _ExpensesState extends State<Expenses> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: const [
Text('The chart'),
Text('Expenses list...'),
],
),
);
}
}
```
### step 2 input的資料處理
#### 檔案結構
```
lib
|-models
|-expense.dart
|-expenses.dart
|-main.dart
```
#### 程式碼構建
我們希望可以方便管理我們的支出(以下面為例,例如說我們目前只有字個分項food, travel, leisure, work,如果你想增加類別例如社交,你可以自己在增加一項social),所以我們希望搭建一個數據結構去進行管理,然後用uuid 的套件,用來生成唯一識別碼(UUIDs)
enum 是一種在程式語言中表示列舉型別(Enumeration Type)的概念,在 Dart 語言中,列舉型別的成員預設情況下會從 0 開始自動編號,也就是 food 的索引值是 0、travel 是 1、leisure 是 2、work 是 3
:::danger
要在你下方的終端機下指令 flutter pub add uuid
:::
**models/expense.dart**
```c=
import 'package:uuid/uuid.dart';
const uuid = Uuid();
enum Category { food, travel, leisure, work }
class Expense {
Expense({
required this.title,
required this.amount,
required this.date,
required this.category,
}) : id = uuid.v4();
final String id;
final String title;
final double amount;
final DateTime date;
final Category category;
}
```
### step 3 創建虛擬費用
#### 檔案結構
```
lib
|-models
|-expense.dart
|-expenses.dart
|-main.dart
```
#### 程式碼構建
我們已經決定好input的資料的結構了,那現在我們試著生成一些範例input方便我們開發時觀察樣式
**lib/expenses.dart**
```c=
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
class Expenses extends StatefulWidget {
const Expenses({super.key});
@override
State<Expenses> createState() {
return _ExpensesState();
}
}
//我們新增的部分
class _ExpensesState extends State<Expenses> {
final List<Expense> _registeredExpenses = [
Expense(
title: 'Flutter Course',
amount: 19.99,
date: DateTime.now(),
category: Category.work,
),
Expense(
title: 'Cinema',
amount: 15.69,
date: DateTime.now(),
category: Category.leisure,
),
];
//到這裡
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: const [
Text('The chart'),
Text('Expenses list...'),
],
),
);
}
}
```
### step 4 listview
#### 檔案結構
```
lib
|-models
|-expense.dart
|-expenses_list.dart
|-expenses.dart
|-main.dart
```
#### 程式碼構建
什麼listview? 在一個螢幕有次序的呈現各個內容,有點像是我們line可以滑上滑下看到我的聊天室。那我們想要將這個表現方式,納入我們的app
[list view](https://book.flutterchina.club/chapter6/listview.html#_6-3-2-listview-builder)
**lib/expenses_lists.dart**
```c=
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
class ExpensesList extends StatelessWidget {
const ExpensesList({
super.key,
required this.expenses,
});
final List<Expense> expenses;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: expenses.length,
itemBuilder: (ctx, index) => Text(expenses[index].title),
);
}
}
```
:::info
這個東西
```c=
itemBuilder: (ctx, index) => Text(expenses[index].title),
```
其實可以這樣寫
```c=
Widget _buildExpenseItem(BuildContext ctx, int index) {
return Text(expenses[index].title);
}
// ...
ListView.builder(
itemCount: expenses.length,
itemBuilder: _buildExpenseItem,
)
```
:::
**lib/expenses.dart**
```c=
//記得加上這條
import 'package:expense_tracker/expenses_list.dart';
.........
//到這裡
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
const Text('The chart'),
//Text('Expenses list...'),
Expanded(
child: ExpensesList(expenses: _registeredExpenses),
),
],
),
);
}
}
```
小實驗:試試看拿掉Expanded().....
### step 5 formating
#### 檔案結構
```
lib
|-models
|-expense.dart
|-widgets
|-expenses_list
|-expenses_list.dart
|-expenses_item.dart
|-expenses.dart
|-main.dart
```
#### 程式碼構建
美化我們的方塊,除了加上一些格式,我們還要改變時間的顯示以及加上icons,我們會用到formattedDate
https://stackoverflow.com/questions/51579546/how-to-format-datetime-in-flutter
:::danger
要在你下方的終端機下指令 flutter pub add intl
:::
**lib/models/expense.dart**
```c=
//別忘了
import 'package:intl/intl.dart';//new
.........
final formatter = DateFormat.yMd();//new
const uuid = Uuid();
//新增以下
enum Category { food, travel, leisure, work }
const categoryIcons = {
Category.food: Icons.lunch_dining,
Category.travel: Icons.flight_takeoff,
Category.leisure: Icons.movie,
Category.work: Icons.work,
};
class Expense {
Expense({
required this.title,
required this.amount,
required this.date,
required this.category,
}) : id = uuid.v4();
final String id;
final String title;
final double amount;
final DateTime date;
final Category category;
//跟這裡
String get formattedDate {
return formatter.format(date);
}
........
```
```c=
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
class ExpenseItem extends StatelessWidget {
const ExpenseItem(this.expense, {super.key});
final Expense expense;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
child: Column(
children: [
Text(expense.title),
const SizedBox(height: 4),
Row(
children: [
Text('\$${expense.amount.toStringAsFixed(2)}'),
const Spacer(),
Row(
children: [
Icon(categoryIcons[expense.category]),
const SizedBox(width: 8),
Text(expense.formattedDate),
],
),
],
),
],
),
),
);
}
}
```
**lib/widgets/expenses_list/expenses_list.dart**
```c=
.........
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: expenses.length,
//itemBuilder: (ctx, index) => Text(expenses[index].title),
itemBuilder: (ctx, index) => ExpenseItem(expenses[index]),
);
}
}
```

### step 6 App bar
#### 檔案結構
```
lib
|-models
|-expense.dart
|-widgets
|-expenses_list
|-expenses_list.dart
|-expenses_item.dart
|-expenses.dart
|-main.dart
```
#### 程式碼構建
現在我們有了這些顯示支出的方塊了,我們需要讓使用者可以自己填入方塊。我們的做法是在這些方塊上面新增一個app bar,那我們需要在建構一個row之類的來表示這個方塊ㄇ?不需要Scaffold裡面有提供這項功能
**lib/models/expense.dart**
```c=
Widget build(BuildContext context) {
return Scaffold(
//從這裡
appBar: AppBar(
title: const Text('Flutter ExpenseTracker'),
actions: [
IconButton(
onPressed: (){},
icon: const Icon(Icons.add),
),
],
),
//新增
body: Column(
children: [
const Text('The chart'),
```
你就會有一個app bar 跟一個加號了

### step 7 App bar + 新增功能建立
#### 檔案結構
```
lib
|-models
|-expense.dart
|-widgets
|-expenses_list
|-expenses_list.dart
|-expenses_item.dart
|-expenses.dart
|-main.dart
```
#### 程式碼構建
有了剛剛的加號,我們現在要讓這個加號有功能,我們需要一個函示在我們點擊時被觸發
``` c=
Expense(
title: 'Cinema',
amount: 15.69,
date: DateTime.now(),
category: Category.leisure,
),
];
//新增
void _openAddExpenseOverlay() {
showModalBottomSheet(
context: context,
builder: (ctx) => const NewExpense(),//step 8
);
}
//的部分
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter ExpenseTracker'),
actions: [
IconButton(
onPressed: _openAddExpenseOverlay,//這裡記得改成函式名稱
icon: const Icon(Icons.add),
),
```
### step 8 新增一個輸入匡
#### 檔案結構
```
lib
|-models
|-expense.dart
|-widgets
|-expenses_list
|-expenses_list.dart
|-expenses_item.dart
|-expenses.dart
|-new_expenses.dart
|-main.dart
```
#### 程式碼構建
從上一個step我們知道_openAddExpenseOverlay會將NewExpense()觸發,所以我們來實踐這個function
**NewExpense()**
關於[TextEditingController](https://flutter.cn/docs/cookbook/forms/text-field-changes)的解釋
關於[dispose](https://tw-hkt.blogspot.com/2019/08/flutter-flutter.html)的解釋
``` c=
import 'package:flutter/material.dart';
class NewExpense extends StatefulWidget {
const NewExpense({super.key});
@override
State<NewExpense> createState() {
return _NewExpenseState();
}
}
class _NewExpenseState extends State<NewExpense> {
final _titleController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(//文字輸入框元件
controller: _titleController,
maxLength: 50,
decoration: const InputDecoration(
label: Text('Title'),
),
),
Row(
children: [
ElevatedButton(
onPressed: () {
print(_titleController.text);//可以從output看你輸入的東西有沒有出現
},
child: const Text('Save Expense'),
),
],
),
],
),
);
}
}
```

### step 9 作業 新增一個輸入匡輸入金額

new expense(下面有參考答案)
```c=
import 'package:flutter/material.dart';
class NewExpense extends StatefulWidget {
const NewExpense({super.key});
@override
State<NewExpense> createState() {
return _NewExpenseState();
}
}
class _NewExpenseState extends State<NewExpense> {
final _titleController = TextEditingController();
//hint: 添加一个 TextEditingController 来控制金额文本字段
@override
void dispose() {
_titleController.dispose();
//hint: 释放金额文本字段的控制器
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _titleController,
maxLength: 50,
decoration: const InputDecoration(
label: Text('Title'),
),
),
//hint: 添加一个 TextField 来输入金额,使用 _amountController 控制器
Row(
children: [
TextButton(
onPressed: () {},
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
print(_titleController.text);
//hint: 打印金额文本字段的内容
},
child: const Text('Save Expense'),
),
],
),
],
),
);
}
}
```
:::spoiler
```c=
import 'package:flutter/material.dart';
class NewExpense extends StatefulWidget {
const NewExpense({super.key});
@override
State<NewExpense> createState() {
return _NewExpenseState();
}
}
class _NewExpenseState extends State<NewExpense> {
final _titleController = TextEditingController();
//hint: 添加一个 TextEditingController 来控制金额文本字段
final _amountController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
//hint: 释放金额文本字段的控制器
_amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _titleController,
maxLength: 50,
decoration: const InputDecoration(
label: Text('Title'),
),
),
//hint: 添加一个 TextField 来输入金额,使用 _amountController 控制器
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
prefixText: '\$ ',
label: Text('Amount'),
),
),
Row(
children: [
TextButton(
onPressed: () {},
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
print(_titleController.text);
//hint: 打印金额文本字段的内容
print(_amountController.text);
},
child: const Text('Save Expense'),
),
],
),
],
),
);
}
}
```
:::
### step 10 date picker

```c=
class _NewExpenseState extends State<NewExpense> {
final _titleController = TextEditingController();
final _amountController = TextEditingController();
DateTime? _selectedDate;
void _presentDatePicker() async {
final now = DateTime.now();
final firstDate = DateTime(now.year - 1, now.month, now.day);
final pickedDate = await showDatePicker(
context: context,
initialDate: now,
firstDate: firstDate,
lastDate: now,
);
setState(() {
_selectedDate = pickedDate;
});
}
@override
void dispose() {
_titleController.dispose();
_amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _titleController,
maxLength: 50,
decoration: const InputDecoration(
label: Text('Title'),
),
),
Row(
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
prefixText: '\$ ',
label: Text('Amount'),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
_selectedDate == null
? 'No date selected'
: formatter.format(_selectedDate!),
),
IconButton(
onPressed: _presentDatePicker,
icon: const Icon(
Icons.calendar_month,
),
),
],
),
),
],
),
Row(
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
print(_titleController.text);
print(_amountController.text);
},
child: const Text('Save Expense'),
),
],
),
],
),
);
}
}
```
小問題:為什麼這裡需要使用 await?