---
# System prepended metadata

title: 記帳程式

---

# 記帳程式



### 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: ...,
    ),
  );
}
```
![截圖 2024-01-09 下午3.01.00](https://hackmd.io/_uploads/SkDV1d5dT.png =40%x)

在我們的介面上，我們有圖表（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]),
    );
  }
}
```
![截圖 2024-01-17 晚上11.06.40](https://hackmd.io/_uploads/rJxG6wSta.png)
### 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 跟一個加號了
![截圖 2024-02-25 晚上10.06.45](https://hackmd.io/_uploads/B1fKKTdha.png)
### 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'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

```
![截圖 2024-03-12 下午1.21.38](https://hackmd.io/_uploads/ryj_8waaa.png)
### step 9 作業 新增一個輸入匡輸入金額
![截圖 2024-03-12 下午2.45.29](https://hackmd.io/_uploads/SJj75OaTa.png)
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
![截圖 2024-03-12 下午3.58.09](https://hackmd.io/_uploads/rJgSiFTTa.png)
```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?
