大家好,歡迎來到第十五天,也是我們「省錢拍拍」專案第二篇章:AI 賦能的正式開端!
在過去兩週,我們從零到一,打造了一個功能完備、架構清晰的雲端記帳 App。現在,我們要為它注入智慧。我們的終極目標是實現一個流暢的「智慧掃描」體驗:使用者拍下發票,App 自動辨識金額與品項,並由 AI 建議分類,最後一鍵儲存。
今天,我們將完成此目標的第一步:讓 App 睜開它的眼睛。我們將整合 Flutter 強大的 image_picker
套件,讓 App 能喚醒相機拍照,或從相簿選取照片,並將圖片顯示於畫面上。
image_picker
是 Flutter 官方維護的套件中,最受歡迎也最常用於處理圖片選擇的工具之一。
在終端機底下輸入指令,它會自動抓取最新穩定版並加入 pubspec.yaml。:
flutter pub add image_picker
任何需要存取手機硬體(如相機、相簿)的 App,都必須向作業系統請求權限,並告知使用者為何需要此權限。這一步是新手最容易遺漏而出錯的地方。
Android 設定
image_picker
新版本已將權限請求整合得很好,通常不需在AndroidManifest.xml
中手動添加<uses-permission>
。因此,我們暫時不需對 Android 進行額外設定。
iOS 設定
打開專案中的 ios/Runner/Info.plist
檔案,在根層級的 <dict>
標籤內,加入以下兩個鍵值對,向使用者說明權限用途:
<key>NSPhotoLibraryUsageDescription</key>
<string>我們需要您的相簿權限,以便您能選擇發票照片進行記帳。</string>
<key>NSCameraUsageDescription</key>
<string>我們需要您的相機權限,以便您能拍攝發票照片進行記帳。</string>
為了提供更好的使用者體驗,我們建立一個新頁面來處理拍照和預覽。
在 lib
資料夾下建立一個新檔案 scan_page.dart
。
// lib/scan_page.dart
import 'dart:io'; // 需要導入 dart:io 來使用 File 類別
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class ScanPage extends StatefulWidget {
const ScanPage({super.key});
@override
State<ScanPage> createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
// 用來存放使用者選擇的圖片檔案
File? _imageFile;
final ImagePicker _picker = ImagePicker();
// 函式:從相機拍照
Future<void> _takePicture() async {
final XFile? image = await _picker.pickImage(source: ImageSource.camera);
if (image != null) {
setState(() {
_imageFile = File(image.path);
});
}
}
// 函式:從相簿選取
Future<void> _pickImageFromGallery() async {
final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
_imageFile = File(image.path);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('掃描發票')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 預覽圖片的區塊
Expanded(
child: _imageFile == null
? const Center(child: Text('尚未選擇圖片'))
: Container(
padding: const EdgeInsets.all(16.0),
child: Image.file(_imageFile!),
),
),
// 按鈕區塊
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton.icon(
onPressed: _takePicture,
icon: const Icon(Icons.camera_alt),
label: const Text('開啟相機'),
),
ElevatedButton.icon(
onPressed: _pickImageFromGallery,
icon: const Icon(Icons.photo_library),
label: const Text('從相簿選取'),
),
],
),
),
],
),
),
);
}
}
程式碼解析:
_imageFile
狀態變數 (型別為 File?
) 來保存圖片。ImagePicker().pickImage()
是 image_picker
的核心方法,source
參數決定開啟相機 (ImageSource.camera
) 還是相簿 (ImageSource.gallery
)。XFile
物件,我們使用 File(image.path)
將其轉換為標準的 File
物件。setState
中更新 _imageFile
,以觸發畫面重建。Image.file(_imageFile!)
來顯示本地圖片檔案。最後,回到 lib/main.dart
,找到我們之前建立的「掃描發票」按鈕,為它加上導航到 ScanPage
的功能。
// lib/main.dart -> _HomePageState -> build -> 中間功能按鈕區塊
import 'package:snapsaver/scan_page.dart'; // 記得導入新頁面
// ...
// 找到代表「掃描發票」的那個 Column
child: GestureDetector( // 用 GestureDetector 或 InkWell 包裹起來
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ScanPage()),
);
},
child: const Column(
children: [
Icon(Icons.camera_alt, size: 40, color: Colors.teal), // 顏色可以跟主題色同步
Text('掃描發票'),
],
),
),
// ...
可將
Column
包在GestureDetector
或InkWell
裡。InkWell
能提供點擊時的水波紋回饋效果。
現在重啟 App。點擊主頁的「掃描發票」按鈕,應該能成功跳轉到新頁面,並從那裡喚醒相機或相簿了!選擇圖片後,它會顯示在螢幕中央。
今天我們成功地在 App 與手機硬體之間建立了第一座橋樑!我們的 App 終於「睜開了眼睛」,能夠接收來自真實世界的視覺資訊。我們學會了:
image_picker
套件。現在,我們手中握有一張充滿資訊的發票圖片,但對 App 而言,它還只是一堆無意義的像素點。
明天,我們將賦予 App「閱讀」的能力。我們將深入探索 Google ML Kit,實作光學字元辨識 (OCR) 功能,將圖片中的文字一個個提取出來,把像素轉化為有價值的數據!