Expo SDK

Expo が提供するネイティブ機能のラッパ群。カメラ、GPS、通知、ファイル、認証、センサーなど、 モバイルアプリで必要になるほぼすべてが「expo-*」のパッケージとして揃っている。

共通の流れ

  1. npx expo install expo-xxx でインストール
  2. app.jsonpluginsconfig plugin を追加(必要な場合)
  3. パーミッション文言を設定(写真、カメラ、位置、通知など)
  4. JS から API を呼ぶ。多くは requestPermissionsAsync() から始める

プロジェクトに新しいネイティブモジュールを追加した場合、Expo Go では動かないので Dev Build を作り直す必要がある。

パーミッション

ほぼ全ての機能で OS レベルの許可が必要。パーミッション文言("なぜ必要か")app.json の各プラグイン options に書く。書かないとストア審査で落ちる。

app.json の例
{
  "expo": {
    "plugins": [
      "expo-router",
      ["expo-camera", { "cameraPermission": "プロフィール画像を撮影します" }],
      ["expo-location", { "locationAlwaysAndWhenInUsePermission": "近くのスポットを表示します" }],
      ["expo-image-picker", { "photosPermission": "写真をアップロードします" }]
    ]
  }
}

カメラ・写真ライブラリ

expo-camera

ライブカメラビュー。QR コードスキャン、写真撮影、動画撮影。

import { CameraView, useCameraPermissions } from "expo-camera"

function Scanner() {
  const [permission, requestPermission] = useCameraPermissions()

  if (!permission) return null
  if (!permission.granted) return (
    <Pressable onPress={requestPermission}><Text>許可</Text></Pressable>
  )

  return (
    <CameraView
      style={{ flex: 1 }}
      facing="back"
      onBarcodeScanned={({ data }) => console.log("scanned:", data)}
      barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
    />
  )
}

expo-image-picker

ユーザーに写真ライブラリから選択させるか、カメラを起動して撮影させる。

import * as ImagePicker from "expo-image-picker"

async function pick() {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ["images"],
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  })
  if (!result.canceled) {
    const uri = result.assets[0].uri
    upload(uri)
  }
}

expo-media-library

撮影した画像をカメラロールに保存したり、ライブラリ全体にアクセスする時。

位置情報

expo-location。GPS、ヘディング、ジオコーディング、バックグラウンド位置情報まで。

import * as Location from "expo-location"

async function getLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync()
  if (status !== "granted") return

  const loc = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.Balanced,
  })
  console.log(loc.coords.latitude, loc.coords.longitude)
}

// 連続的な追跡
const sub = await Location.watchPositionAsync(
  { accuracy: Location.Accuracy.High, distanceInterval: 10 },
  (location) => { /* 10m ごと */ },
)
sub.remove()  // 終わったら必ず

バックグラウンド位置情報はストア審査が厳しい。本当に必要かどうか吟味する。

通知(Push / Local)

expo-notificationsローカル通知(時刻指定)プッシュ通知(リモート)の両方。

ローカル通知

import * as Notifications from "expo-notifications"

async function scheduleAlarm() {
  await Notifications.requestPermissionsAsync()
  await Notifications.scheduleNotificationAsync({
    content: { title: "起きる時間", body: "おはよう" },
    trigger: { seconds: 60 * 60 * 8 },  // 8 時間後
  })
}

プッシュ通知(Expo Push)

APNs / FCM のラッパとしてExpo の Push サービスが使える。トークンを取ってサーバに送り、サーバから Expo のエンドポイントに POST するだけ。

const token = (await Notifications.getExpoPushTokenAsync()).data
// → サーバへ保存

// サーバ側
fetch("https://exp.host/--/api/v2/push/send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    to: token,
    title: "新しいメッセージ",
    body: "..."
  }),
})

独自に APNs / FCM を直接叩きたい場合は別のセットアップ(証明書、サービスアカウント)が必要。

FileSystem

アプリ専用ディレクトリの読み書き、ダウンロード、アップロード。

import * as FileSystem from "expo-file-system"

const path = FileSystem.documentDirectory + "data.json"

// 書き込み
await FileSystem.writeAsStringAsync(path, JSON.stringify({ ok: true }))

// 読み込み
const text = await FileSystem.readAsStringAsync(path)

// ダウンロード
const result = await FileSystem.downloadAsync(
  "https://example.com/big.zip",
  FileSystem.documentDirectory + "big.zip",
)
console.log(result.uri)

ディレクトリの種類:

センサー

加速度・ジャイロ・地磁気・歩数計など。expo-sensors

import { Accelerometer } from "expo-sensors"

const sub = Accelerometer.addListener(({ x, y, z }) => {
  console.log(x, y, z)
})
Accelerometer.setUpdateInterval(100)  // 100ms

// 終わったら
sub.remove()

歩数計

import { Pedometer } from "expo-sensors"

const isAvailable = await Pedometer.isAvailableAsync()
const result = await Pedometer.getStepCountAsync(start, end)

音声・動画

expo-audio / expo-video

expo-av は分割され、expo-audioexpo-video が現在の標準

import { useAudioPlayer } from "expo-audio"

function Player() {
  const player = useAudioPlayer(require("../assets/click.mp3"))
  return <Pressable onPress={() => player.play()}><Text>再生</Text></Pressable>
}
import { VideoView, useVideoPlayer } from "expo-video"

function Watch() {
  const player = useVideoPlayer("https://example.com/video.mp4", (p) => {
    p.loop = true
    p.play()
  })
  return <VideoView player={player} style={{ width: "100%", height: 240 }} contentFit="cover" />
}

触覚(バイブ)

import * as Haptics from "expo-haptics"

await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
await Haptics.selectionAsync()

クリップボード

import * as Clipboard from "expo-clipboard"

await Clipboard.setStringAsync("コピーされる文字")
const text = await Clipboard.getStringAsync()

共有

import * as Sharing from "expo-sharing"

if (await Sharing.isAvailableAsync()) {
  await Sharing.shareAsync(localFileUri)
}

生体認証(Face ID / Touch ID)

import * as LocalAuthentication from "expo-local-authentication"

const has = await LocalAuthentication.hasHardwareAsync()
const enrolled = await LocalAuthentication.isEnrolledAsync()
if (has && enrolled) {
  const result = await LocalAuthentication.authenticateAsync({
    promptMessage: "ロック解除",
  })
  if (result.success) { /* 通過 */ }
}

外部ブラウザ・WebView

expo-web-browser

SFSafariViewController(iOS)/ Custom Tabs(Android)でリンクを開く。 OAuth コールバックを取りたい時に使う。

import * as WebBrowser from "expo-web-browser"

const result = await WebBrowser.openAuthSessionAsync(
  "https://auth.example.com/login",
  "myapp://callback",
)
if (result.type === "success") {
  const url = result.url
  // url からトークン抽出
}

WebView をアプリ内に埋め込む

react-native-webview パッケージ(Expo SDK ではなく定番のサードパーティ)。

OS / デバイス情報

バックグラウンドタスク

モバイル OS はバックグラウンド処理に厳しい。「30 分ごとに必ず動く」は保証されない。 重要な処理はサーバ側 + プッシュ通知で実装するのが定石。

位置・地図

実用例: 写真投稿フロー

import * as ImagePicker from "expo-image-picker"
import * as FileSystem from "expo-file-system"

async function uploadPhoto() {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ["images"],
    quality: 0.7,
  })
  if (result.canceled) return

  const uri = result.assets[0].uri
  const base64 = await FileSystem.readAsStringAsync(uri, {
    encoding: FileSystem.EncodingType.Base64,
  })

  await fetch("/api/photos", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ image: base64 }),
  })
}
SDK の探し方

公式の Expo SDK Reference に全パッケージが網羅されている。
機能名で検索すれば大抵見つかる。「自前でネイティブモジュールを書く前にここをチェック」が鉄則。