iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
自我挑戰組

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

Day 15: 智慧掃描第一步 - 整合 image_picker 喚醒相機

  • 分享至 

  • xImage
  •  

前言

大家好,歡迎來到第十五天,也是我們「省錢拍拍」專案第二篇章:AI 賦能的正式開端!

在過去兩週,我們從零到一,打造了一個功能完備、架構清晰的雲端記帳 App。現在,我們要為它注入智慧。我們的終極目標是實現一個流暢的「智慧掃描」體驗:使用者拍下發票,App 自動辨識金額與品項,並由 AI 建議分類,最後一鍵儲存。

今天,我們將完成此目標的第一步:讓 App 睜開它的眼睛。我們將整合 Flutter 強大的 image_picker 套件,讓 App 能喚醒相機拍照,或從相簿選取照片,並將圖片顯示於畫面上。

Step 1: 加入 image_picker 套件

image_picker 是 Flutter 官方維護的套件中,最受歡迎也最常用於處理圖片選擇的工具之一。

在終端機底下輸入指令,它會自動抓取最新穩定版並加入 pubspec.yaml。:

flutter pub add image_picker

Step 2: 設定平台權限 (至關重要!)

任何需要存取手機硬體(如相機、相簿)的 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>

Step 3: 建立一個專門的掃描頁面

為了提供更好的使用者體驗,我們建立一個新頁面來處理拍照和預覽。

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('從相簿選取'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

程式碼解析:

  1. 我們建立了一個 _imageFile 狀態變數 (型別為 File?) 來保存圖片。
  2. ImagePicker().pickImage()image_picker 的核心方法,source 參數決定開啟相機 (ImageSource.camera) 還是相簿 (ImageSource.gallery)。
  3. 該方法會回傳一個 XFile 物件,我們使用 File(image.path) 將其轉換為標準的 File 物件。
  4. setState 中更新 _imageFile,以觸發畫面重建。
  5. UI 上,我們使用 Image.file(_imageFile!) 來顯示本地圖片檔案。

Step 4: 從主頁面觸發掃描功能

最後,回到 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 包在 GestureDetectorInkWell 裡。InkWell 能提供點擊時的水波紋回饋效果。

現在重啟 App。點擊主頁的「掃描發票」按鈕,應該能成功跳轉到新頁面,並從那裡喚醒相機或相簿了!選擇圖片後,它會顯示在螢幕中央。

掃描發票頁面

掃完顯示在中間

今日結語

今天我們成功地在 App 與手機硬體之間建立了第一座橋樑!我們的 App 終於「睜開了眼睛」,能夠接收來自真實世界的視覺資訊。我們學會了:

  1. 如何整合 image_picker 套件。
  2. 建立一個獨立的頁面來處理圖片的選擇與預覽。

現在,我們手中握有一張充滿資訊的發票圖片,但對 App 而言,它還只是一堆無意義的像素點。

明天,我們將賦予 App「閱讀」的能力。我們將深入探索 Google ML Kit,實作光學字元辨識 (OCR) 功能,將圖片中的文字一個個提取出來,把像素轉化為有價值的數據!


上一篇
Day 14: 補完 CRUD 最後一哩路 - 實作更新與滑動刪除
下一篇
Day 16: 讓 App 開口說話 - 整合 ML Kit 實現圖片文字辨識
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言