大家好,歡迎來到第十九天!在過去的幾天裡,我們一步步為「省錢拍拍」賦予了超能力:它學會了使用相機 (image_picker
)、學會了閱讀 (ML Kit OCR
),更學會了思考與整理 (Gemini API
+ Prompt Engineering
)。
我們現在已經能在 ScanPage 上,看著 AI 精準地從一張模糊的發票圖片中,提取出結構化的店家名稱、金額與分類。但這一切的終點,不該只是停留在畫面上的一個資訊展示。
今天,我們將打通整個智慧流程的「最後一哩路」。我們要將這些 AI 分析出的資料,無縫地傳遞到「新增消費」頁面,並自動填入表單。我們的目標是實現一個真正流暢、神奇的使用者體驗:使用者只需拍照、確認,即可完成一筆記帳。
我們的 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
和 _amountController
的 text
。_submitForm
和 build
方法完全不用動!這得益於我們之前良好的架構設計。現在,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
參數。現在,重新啟動 App,讓我們來體驗一下這個完整的智慧流程:
ScanPage
,拍照或從相簿選取一張發票。使用者現在需要做的,僅僅是確認一下資料是否正確,然後按下「儲存」,一筆由 AI 輔助的消費紀錄就輕鬆存入雲端了。
今天我們打通了整個智慧掃描流程的任督二脈,將 AI 的分析能力,轉化為了實實在在的自動化操作。我們學會了:
Map
)。明天,我們將探索 Gemini 更進階的應用,讓它從一個「資料擷取員」升級為一位「消費分析師」。我們將學習如何將多筆消費紀錄一次性發送給 AI,讓它為我們產生個人化的財務摘要與省錢建議!