iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
自我挑戰組

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

Day 19: 串連一切 - 實現一鍵掃描,智慧填單

  • 分享至 

  • xImage
  •  

前言

大家好,歡迎來到第十九天!在過去的幾天裡,我們一步步為「省錢拍拍」賦予了超能力:它學會了使用相機 (image_picker)、學會了閱讀 (ML Kit OCR),更學會了思考與整理 (Gemini API + Prompt Engineering)。

我們現在已經能在 ScanPage 上,看著 AI 精準地從一張模糊的發票圖片中,提取出結構化的店家名稱、金額與分類。但這一切的終點,不該只是停留在畫面上的一個資訊展示。

今天,我們將打通整個智慧流程的「最後一哩路」。我們要將這些 AI 分析出的資料,無縫地傳遞到「新增消費」頁面,並自動填入表單。我們的目標是實現一個真正流暢、神奇的使用者體驗:使用者只需拍照、確認,即可完成一筆記帳。

Step 1: 升級 AddTransactionPage - 讓表單學會接收「預填資料」

我們的 AddTransactionPage 目前有兩種模式:完全空白的「新增模式」,以及傳入 Transaction 物件的「編輯模式」。現在,我們要為它加入第三種能力:「預填模式」。

我們將修改它的建構子,讓它能接收一個 Map 型別的初始資料。

打開 lib/add_transaction_page.dart 進行修改:

// lib/add_transaction_page.dart
// ...
class AddTransactionPage extends StatefulWidget {
  final Transaction? transaction;
  // 1. 新增一個可選的 initialData 參數
  final Map<String, dynamic>? initialData;

  const AddTransactionPage({
    super.key,
    this.transaction,
    this.initialData,
  });

  @override
  State<AddTransactionPage> createState() => _AddTransactionPageState();
}

class _AddTransactionPageState extends State<AddTransactionPage> {
  // ... controllers, formKey, service ...
  
  bool get isEditMode => widget.transaction != null;

  @override
  void initState() {
    super.initState();
    if (isEditMode) {
      // 編輯模式的邏輯不變
      _titleController.text = widget.transaction!.title;
      _amountController.text = widget.transaction!.amount.toString();
    } else if (widget.initialData != null) {
      // 2. 如果有初始資料 (預填模式),則用它來設定 controller 的初始值
      _titleController.text = widget.initialData!['storeName']?.toString() ?? '';
      _amountController.text = widget.initialData!['totalAmount']?.toString() ?? '';
      // 注意:暫時不預填分類,因為分類欄位尚未做成下拉選單
    }
  }
  
  // ... _submitForm() 和 build() 方法完全不需要改變! ...
}
  • 我們在建構子中加入了一個可選的 initialData 參數。
  • initState 方法中,我們增加了 else if 判斷。如果不是編輯模式,但收到了 initialData,就用這份資料來初始化 _titleController_amountControllertext
  • 我們的 _submitFormbuild 方法完全不用動!這得益於我們之前良好的架構設計。

Step 2: 在 ScanPage 打通最後的橋樑

現在,AddTransactionPage 已經準備好接收預填資料了。我們只需要在 ScanPage 的分析結果下方,加入一個「使用此結果記帳」的按鈕,並在點擊時觸發帶有參數的導航。

打開 lib/scan_page.dart,修改 _buildResultDisplay 輔助函式:

// lib/scan_page.dart
// 導入 AddTransactionPage
import 'package:snapsaver/add_transaction_page.dart';
// ...

class _ScanPageState extends State<ScanPage> {
  // ...
  
  Widget _buildResultDisplay() {
    if (_isProcessing) { /* ... */ }

    if (_geminiResponse != null) {
      // 檢查是否有錯誤
      if (_geminiResponse!.containsKey('error')) {
        return Text(/* ... 顯示錯誤 ... */);
      }
      
      final storeName = _geminiResponse!['storeName'] ?? '未知店家';
      final totalAmount = _geminiResponse!['totalAmount'] ?? '未知金額';
      final category = _geminiResponse!['category'] ?? '未分類';

      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('AI 分析結果:', style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 8),
          Text('店家名稱:$storeName', /* ... */),
          Text('總金額:$totalAmount', /* ... */),
          Text('消費分類:$category', /* ... */),
          
          // --- 關鍵新增 ---
          const SizedBox(height: 24),
          SizedBox(
            width: double.infinity, // 讓按鈕撐滿寬度
            child: ElevatedButton.icon(
              icon: const Icon(Icons.edit_document),
              label: const Text('使用此結果記帳'),
              onPressed: () {
                // 點擊後,導航到 AddTransactionPage
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => AddTransactionPage(
                      // 並將 AI 分析結果作為 initialData 傳入
                      initialData: _geminiResponse,
                    ),
                  ),
                );
              },
            ),
          ),
          // --- 新增結束 ---

          const Divider(height: 24),
          Text('原始辨識文字:\n${_recognizedText ?? ''}', /* ... */),
        ],
      );
    }
    // ...
    return const Text('點擊「開始辨識」按鈕進行分析...');
  }
  
  // ...
}
  • Column 的底部新增了一個 ElevatedButton
  • 這個按鈕的 onPressed 回呼函式執行了本次串連最核心的操作:Navigator.push
  • 建立 AddTransactionPage 的實例時,我們將 _geminiResponse 這個 Map 物件,直接傳遞給了它的 initialData 參數。

Step 3: 見證奇蹟的時刻!

現在,重新啟動 App,讓我們來體驗一下這個完整的智慧流程:

  1. 在主頁點擊「掃描發票」。
  2. 進入 ScanPage,拍照或從相簿選取一張發票。
  3. 點擊右下角的「開始辨識與分析」按鈕。
  4. 等待片刻,看到 AI 精準地分析出店家、金額與分類。
  5. 點擊下方新增的「使用此結果記帳」按鈕。
  6. App 會自動跳轉到「新增消費」頁面,而且「品項名稱」和「金額」兩個欄位已經被完美地填好了!

https://ithelp.ithome.com.tw/upload/images/20251003/20163912prbeTuyIft.png

https://ithelp.ithome.com.tw/upload/images/20251003/2016391262al67oWds.png

使用者現在需要做的,僅僅是確認一下資料是否正確,然後按下「儲存」,一筆由 AI 輔助的消費紀錄就輕鬆存入雲端了。

今日結語

今天我們打通了整個智慧掃描流程的任督二脈,將 AI 的分析能力,轉化為了實實在在的自動化操作。我們學會了:

  1. 如何改造現有頁面,讓它能夠接收並處理預填資料。
  2. 在頁面導航時,如何透過建構子傳遞複雜的資料(例如 Map)。
  3. 將「拍照 -> OCR -> AI 分析 -> 預填表單 -> 儲存」這個複雜的鏈路,串連成一個對使用者而言,流暢、簡單且充滿驚喜的體驗。

明天,我們將探索 Gemini 更進階的應用,讓它從一個「資料擷取員」升級為一位「消費分析師」。我們將學習如何將多筆消費紀錄一次性發送給 AI,讓它為我們產生個人化的財務摘要與省錢建議!


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

尚未有邦友留言

立即登入留言