# 記帳程式 ### 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?