呼叫手機硬體:相機、GPS 與推播

如果你的服務只需要顯示文字和按鈕,那做一個 RWD 手機版網頁就夠了。 我們之所以要大費周章開發「原生 App」,為的就是要存取手機硬體:相機、GPS 定位、推播通知、生物辨識 (Face ID / 指紋) 等等。

在過去的 React Native 開發中,要串接這些硬體功能,你必須去修改底層的 iOS Info.plist 和 Android 的 AndroidManifest.xml,非常痛苦且容易出錯。 好消息是,Expo SDK 幫我們把這些髒活全包了!

在這堂課中,我們將學習三個最常用的硬體功能。

1. 權限請求 (Permissions) 的黃金法則

在存取任何硬體之前,我們都必須遵守蘋果和 Google 的規定:必須先取得使用者的同意授權

如果在未授權的情況下直接呼叫硬體 API,你的 App 會直接閃退 (Crash)。

Expo 大部分的硬體模組都提供了一個類似 usePermissions() 的 Hook,讓我們能優雅地處理這件事:

  1. 檢查目前權限狀態。
  2. 如果尚未詢問,跳出系統對話框詢問使用者。
  3. 如果使用者拒絕,我們必須顯示一段文字告訴他為什麼我們需要這個權限,並引導他去系統設定裡開啟。

2. 實戰一:啟動相機與挑選照片 (Image Picker)

讓使用者拍照或從相簿挑選照片來上傳大頭貼。

安裝套件:

npx expo install expo-image-picker

程式碼實作:

import { useState } from 'react';
import { Button, Image, View, Alert } from 'react-native';
import * as ImagePicker from 'expo-image-picker';

export default function CameraScreen() {
  const [image, setImage] = useState<string | null>(null);

  // 1. 請求相機權限的函數
  const requestPermissionAndPick = async () => {
    const { status } = await ImagePicker.requestCameraPermissionsAsync();
    
    if (status !== 'granted') {
      Alert.alert('需要權限', '我們需要相機權限才能讓你拍照喔!');
      return;
    }

    // 2. 啟動相機
    let result = await ImagePicker.launchCameraAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images, // 限制只能拍照,不能錄影
      allowsEditing: true, // 允許使用者拍完後裁切
      aspect: [4, 3], // 裁切比例
      quality: 0.8, // 壓縮圖片品質 (0-1) 以節省頻寬
    });

    if (!result.canceled) {
      // 3. 將拍好的照片 URI 存入 State 並顯示
      setImage(result.assets[0].uri);
    }
  };

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button title="打開相機拍照" onPress={requestPermissionAndPick} />
      {image && (
        <Image source={{ uri: image }} style={{ width: 300, height: 300, marginTop: 20, borderRadius: 10 }} />
      )}
    </View>
  );
}

提示:如果你想讓使用者從相簿挑照片,只要把 launchCameraAsync 換成 launchImageLibraryAsync 即可!

3. 實戰二:取得 GPS 定位資訊 (Location)

製作像 Uber 或 Foodpanda 那樣的地圖功能,第一步就是要知道使用者現在在哪裡。

安裝套件:

npx expo install expo-location

程式碼實作:

import { useState, useEffect } from 'react';
import { Text, View, ActivityIndicator } from 'react-native';
import * as Location from 'expo-location';

export default function LocationScreen() {
  const [location, setLocation] = useState<Location.LocationObject | null>(null);
  const [errorMsg, setErrorMsg] = useState<string | null>(null);

  useEffect(() => {
    (async () => {
      // 1. 請求定位權限 (前景使用權限)
      let { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') {
        setErrorMsg('拒絕了定位權限,無法提供服務');
        return;
      }

      // 2. 取得當前位置座標
      let currentLocation = await Location.getCurrentPositionAsync({});
      setLocation(currentLocation);
    })();
  }, []);

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      {errorMsg ? (
        <Text style={{ color: 'red' }}>{errorMsg}</Text>
      ) : location ? (
        <>
          <Text>你的緯度:{location.coords.latitude}</Text>
          <Text>你的經度:{location.coords.longitude}</Text>
        </>
      ) : (
        <ActivityIndicator size="large" color="#0000ff" />
      )}
    </View>
  );
}

4. 實戰三:推播通知 (Push Notifications)

推播通知是喚回使用者 (Retargeting) 最強大的武器。 在 Expo 中,我們不需要自己去跟 Apple APNs 或是 Google FCM 奮鬥,我們可以使用 Expo Push Notifications 服務,它幫我們統整了雙平台的發送邏輯!

安裝套件:

npx expo install expo-notifications expo-device

運作原理:

  1. App 第一次啟動時,向系統要求推播權限。
  2. 取得一組專屬的 ExpoPushToken(這就像是這支手機的地址)。
  3. 將這個 Token 存回你的後端資料庫(例如 Supabase)。
  4. 當你想發送推播時,你的後端只要對 Expo 的 API 伺服器發送一個 HTTP 請求,帶上這個 Token,Expo 就會幫你把通知送到使用者的手機上!
import * as Notifications from 'expo-notifications';

// 設定通知在 App 處於前景時的行為
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true, // 會在畫面上方彈出通知
    shouldPlaySound: true, // 會發出叮咚聲
    shouldSetBadge: false,
  }),
});

// 觸發一個本地端推播 (不經過網路,適合用作鬧鐘或提醒)
async function scheduleLocalNotification() {
  await Notifications.scheduleNotificationAsync({
    content: {
      title: "你有一封新訊息!📬",
      body: '這是一則由 Vibe Tutor 教學程式觸發的測試推播。',
      data: { url: '/chat/123' }, // 可以夾帶隱藏資料,點擊通知後可根據此資料跳轉頁面
    },
    trigger: { seconds: 5 }, // 5 秒後觸發
  });
}

[!CAUTION] 推播的陷阱 iOS 模擬器 (Simulator) 不支援接收真正的遠端推播!你必須將 App 編譯並安裝到「實體 iPhone 手機」上才能測試完整的推播功能。

現在你的 App 已經有了眼睛 (相機) 和方向感 (GPS)。在下一章,我們將學習如何管理 App 內部複雜的資料流與 API 串接——狀態管理 (State Management)

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!