iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
自我挑戰組

攜手 AI 從零開始打造一款 Flutter 應用程式系列 第 21

Day 21: 專案打磨 (I) - 專業的分類選擇器

  • 分享至 

  • xImage
  •  

前言

大家好,歡迎來到第二十一天!至此,我們已經完成了從 UI 設計、後端整合到 AI 賦能的完整開發旅程。「省錢拍拍」的核心功能,包括手動記帳、智慧掃描和 AI 分析,都已全數到位。

從今天起,我們將開啟本系列的最終章——專案打磨。我們將扮演「產品優化師」的角色,把焦點從「功能的實現」轉移到「體驗的提升」和「程式碼的健壯性」。

我們第一個要動刀的地方,就是「新增/編輯消費」頁面中的分類欄位。目前,我們在新增資料時,分類是寫死的 ('未分類');在 AI 掃描時,雖然能建議分類,但使用者無法更改。這顯然不夠理想。

今天,我們的目標是:將陽春的分類欄位,改造成一個專業的下拉式選單,提升使用者體驗與資料一致性。

Step 1: 建立唯一的分類標準

要讓使用者選擇,首先我們得有一份固定的分類清單。將清單統一定義,可以確保 App 的所有部分都遵循同一套標準。

  1. lib 資料夾下,建立一個新資料夾 utils
  2. utils 資料夾中,建立一個新檔案 constants.dart
  3. 在檔案中定義分類列表:
// lib/utils/constants.dart

const List<String> kCategories = [
  "餐飲",
  "購物",
  "交通",
  "娛樂",
  "居家",
  "醫療",
  "學習",
  "其他",
];

Step 2: 改造 AddTransactionPage 的 UI

現在,我們回到 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( /* ... 儲存按鈕 ... */ ),
          ],
        ),
      ),
    );
  }
}
  1. 我們新增了 _selectedCategory 狀態變數來追蹤下拉選單的當前值。
  2. initState 中,我們為「編輯模式」和「AI 預填模式」都設定了 _selectedCategory 的初始值,確保資料能被正確帶入。
  3. DropdownButtonFormFielditems 屬性需要一個 List<DropdownMenuItem>,我們使用 .map() 函式從 kCategories 字串列表輕鬆生成。
  4. onChanged 回呼會在使用者做出選擇時更新我們的 _selectedCategory 狀態,並觸發 UI 重建。

Step 3: 更新儲存與編輯的邏輯

UI 完成後,最後一步就是確保選擇的分類能被正確存入 Firestore。

打開 lib/services/firestore_service.dart,修改 addTransactionupdateTransaction 方法,讓它們都能接收 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,不僅大幅改善了使用者體驗,更確保了資料的一致性與準確性。

我們學會了:

  1. 如何使用 DropdownButtonFormField 及其核心屬性。
  2. 如何管理下拉選單的狀態。
  3. 如何將 UI 的變更,同步更新到後端的資料存取邏輯中。

接下來,App 的另一個可優化之處在哪裡呢?是各種操作後的回饋訊息。目前我們的提示(例如刪除成功)還比較零散。明天,我們將建立一個統一的訊息回饋系統,讓 App 與使用者的溝通更一致、更優雅。


上一篇
Day 20: AI 變身理財顧問 - Gemini 的多筆資料分析
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言