大家好,歡迎來到第二十一天!至此,我們已經完成了從 UI 設計、後端整合到 AI 賦能的完整開發旅程。「省錢拍拍」的核心功能,包括手動記帳、智慧掃描和 AI 分析,都已全數到位。
從今天起,我們將開啟本系列的最終章——專案打磨。我們將扮演「產品優化師」的角色,把焦點從「功能的實現」轉移到「體驗的提升」和「程式碼的健壯性」。
我們第一個要動刀的地方,就是「新增/編輯消費」頁面中的分類欄位。目前,我們在新增資料時,分類是寫死的 ('未分類'
);在 AI 掃描時,雖然能建議分類,但使用者無法更改。這顯然不夠理想。
今天,我們的目標是:將陽春的分類欄位,改造成一個專業的下拉式選單,提升使用者體驗與資料一致性。
要讓使用者選擇,首先我們得有一份固定的分類清單。將清單統一定義,可以確保 App 的所有部分都遵循同一套標準。
lib
資料夾下,建立一個新資料夾 utils
。utils
資料夾中,建立一個新檔案 constants.dart
。// lib/utils/constants.dart
const List<String> kCategories = [
"餐飲",
"購物",
"交通",
"娛樂",
"居家",
"醫療",
"學習",
"其他",
];
現在,我們回到 lib/add_transaction_page.dart
,用 DropdownButtonFormField
來實作分類選單。這個 Widget 完美結合了下拉選單的 UI 和表單欄位的驗證功能。
// lib/add_transaction_page.dart
// 導入我們定義的常數
import 'package:snapsaver/utils/constants.dart';
// ...
class _AddTransactionPageState extends State<AddTransactionPage> {
// ... controllers, formKey, service ...
// 1. 新增一個 state 變數來保存當前選擇的分類
String? _selectedCategory;
@override
void initState() {
super.initState();
if (isEditMode) {
// ...
// 編輯模式時,設定初始分類
_selectedCategory = widget.transaction!.category;
} else if (widget.initialData != null) {
// ...
// AI 預填模式時,設定 AI 建議的分類
final categoryFromAI = widget.initialData!['category']?.toString();
// 確保 AI 給的分類在我們的列表中,如果不在,就預設為 "其他"
if (kCategories.contains(categoryFromAI)) {
_selectedCategory = categoryFromAI;
} else {
_selectedCategory = "其他";
}
}
}
// ... _submitForm() ...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( /* ... */ ),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
TextFormField( /* ... 品項名稱 ... */ ),
const SizedBox(height: 16),
// --- 關鍵改造:新增 DropdownButtonFormField ---
DropdownButtonFormField<String>(
value: _selectedCategory,
// 裝飾器,讓它看起來像一個標準的輸入框
decoration: const InputDecoration(
labelText: '消費分類',
border: OutlineInputBorder(),
),
// 遍歷我們的分類常數列表,為每個選項建立一個 DropdownMenuItem
items: kCategories.map((String category) {
return DropdownMenuItem<String>(
value: category,
child: Text(category),
);
}).toList(),
// 當使用者選擇一個新選項時觸發
onChanged: (String? newValue) {
setState(() {
_selectedCategory = newValue;
});
},
// 驗證邏輯
validator: (value) {
if (value == null || value.isEmpty) {
return '請選擇一個分類';
}
return null;
},
),
// --- 改造結束 ---
const SizedBox(height: 16),
TextFormField( /* ... 金額 ... */ ),
const SizedBox(height: 32),
ElevatedButton( /* ... 儲存按鈕 ... */ ),
],
),
),
);
}
}
_selectedCategory
狀態變數來追蹤下拉選單的當前值。initState
中,我們為「編輯模式」和「AI 預填模式」都設定了 _selectedCategory
的初始值,確保資料能被正確帶入。DropdownButtonFormField
的 items
屬性需要一個 List<DropdownMenuItem>
,我們使用 .map()
函式從 kCategories
字串列表輕鬆生成。onChanged
回呼會在使用者做出選擇時更新我們的 _selectedCategory
狀態,並觸發 UI 重建。UI 完成後,最後一步就是確保選擇的分類能被正確存入 Firestore。
打開 lib/services/firestore_service.dart
,修改 addTransaction
和 updateTransaction
方法,讓它們都能接收 category
參數。
// lib/services/firestore_service.dart
class FirestoreService {
// ...
// C for Create: 新增 category 參數
Future<void> addTransaction({
required String userId,
required String title,
required double amount,
required String category, // 新增
}) async {
try {
await _usersCollection.doc(userId).collection('transactions').add({
'title': title,
'amount': amount,
'category': category, // 使用傳入的參數
'date': Timestamp.now(),
});
} catch (e) { /* ... */ }
}
// U for Update: 新增 newCategory 參數
Future<void> updateTransaction({
required String userId,
required String transactionId,
required String newTitle,
required double newAmount,
required String newCategory, // 新增
}) async {
try {
await _usersCollection
.doc(userId)
.collection('transactions')
.doc(transactionId)
.update({
'title': newTitle,
'amount': newAmount,
'category': newCategory, // 使用傳入的參數
});
} catch (e) { /* ... */ }
}
// ...
}
最後,回到 lib/add_transaction_page.dart
的 _submitForm
方法,將 _selectedCategory
傳遞給 service。
// lib/add_transaction_page.dart -> _submitForm
Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
if (isEditMode) {
await _firestoreService.updateTransaction(
userId: user.uid,
transactionId: widget.transaction!.id,
newTitle: _titleController.text,
newAmount: double.parse(_amountController.text),
newCategory: _selectedCategory!, // 傳遞選擇的分類
);
} else {
await _firestoreService.addTransaction(
userId: user.uid,
title: _titleController.text,
amount: double.parse(_amountController.text),
category: _selectedCategory!, // 傳遞選擇的分類
);
}
if (mounted) Navigator.pop(context);
}
}
重新啟動 App。不管是手動新增、編輯,還是透過 AI 掃描預填,你都可以透過專業的下拉選單來選擇或修改消費分類了!
今天,我們邁出了專案打磨的第一步。透過將一個簡單的欄位升級為 DropdownButtonFormField
,不僅大幅改善了使用者體驗,更確保了資料的一致性與準確性。
我們學會了:
DropdownButtonFormField
及其核心屬性。接下來,App 的另一個可優化之處在哪裡呢?是各種操作後的回饋訊息。目前我們的提示(例如刪除成功)還比較零散。明天,我們將建立一個統一的訊息回饋系統,讓 App 與使用者的溝通更一致、更優雅。