OpenAIExpo2025/10/16 16:15

How to add native code to your app with Expo Modules

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

Expo Modulesでネイティブコードを追加する方法(オーディオルートモジュール)

Key Points

  • ローカルExpoモジュール作成
  • getCurrentRouteAsyncとイベント公開
  • 実機でのテスト推奨

Summary

Expo Modulesを使ってアプリにカスタムネイティブ機能を追加する手順を、オーディオ出力ルート検出モジュール(speaker / wiredHeadset / bluetooth / unknown)を例に解説します。ポイントはローカルモジュール作成、TypeScriptでの型定義、ExpoのNativeModule API(AsyncFunction / Events / OnStartObserving / OnStopObserving)を使ったJS⇄ネイティブの橋渡し、そしてiOSではAVAudioSession、AndroidではAudioManagerを使ってルートを取得・監視することです。

Key Points

  • セットアップ

    • 新規プロジェクト: npx create-expo-app@latest <app>(または既存プロジェクト)。
    • 開発ビルド必須(カスタムネイティブがあるため): expo-dev-client をインストールして開発ビルドを作成。
    • ローカルモジュール生成: npx create-expo-module@latest --local(生成先は modules/expo-audio-route)。ネイティブフォルダ(ios/android)は必ずバージョン管理する。
  • JavaScript/TypeScript側

    • 型定義を作成して契約を明確化(ExpoAudioRoute.types.ts:AudioRoute, RouteChangeEvent, イベント型)。
    • ネイティブモジュールを requireNativeModule<...>("ExpoAudioRoute") で読み込む。
    • getCurrentRouteAsync() を AsyncFunction として公開し、イベント名(onAudioRouteChange)を Events() で宣言する。
    • NativeModule<TEvents> を使えば addListener / removeAllListeners は自動提供される。最初のリスナー追加で OnStartObserving が呼ばれ、最後のリスナー削除で OnStopObserving が呼ばれる点を利用する。
  • ネイティブ実装の要点

    • iOS: AVAudioSession を使って現在の出力ルートを判定・監視。
    • Android: AudioManagerAudioDeviceInfo / AudioDeviceCallback を使って現在の出力を判定・監視。OnCreateaudioManager を初期化する。
    • ネイティブ側で取得した状態は4つの文字列(speaker, wiredHeadset, bluetooth, unknown)に正規化して返す。
  • テストと配布

    • シミュレータ/エミュレータではルート変更テストが制限されるので実機での確認を推奨。
    • ローカルモジュールはアプリ内に置くため、ビルド時にネイティブコードが含まれる開発ビルド/リリースビルドを作成して配布する。

Practical checklist for engineers

  • npx create-expo-module --local でモジュールを作成し、不要なView関連ファイルを削除する。
  • TypeScriptで AudioRoute とイベントペイロードを定義する。
  • JS側は requireNativeModuleNativeModule<Events> を使う。
  • ネイティブ側で AsyncFunction("getCurrentRouteAsync")Events("onAudioRouteChange") を定義、OnStartObserving/OnStopObserving で監視を開始/停止する。
  • 実機で動作確認してから配布用ビルドを作る。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

Expo Modulesでアプリにネイティブコードを追加する方法

Expo Modulesでアプリにネイティブコードを追加する方法

このチュートリアルでは、Expo Modulesを使ってReact Nativeアプリにネイティブ機能を追加する方法を説明します。例として「オーディオルート検出モジュール(どこに音声出力が送られているかを判定するモジュール)」を作成し、イベントリスナーの扱い、テスト、配布までの流れをカバーします。

主なポイント:

  • Expoのエコシステムは多くのライブラリを提供しますが、既存のものが要件に合わない場合はカスタムのネイティブ実装が必要になります。
  • Expo Modulesを使えば、Expoの利点を維持しつつアプリ固有のネイティブ機能を追加できます。
  • シミュレータ/エミュレータではオーディオルートの変化を試せる範囲が限定的なので、実機での検証が推奨されます。

事前準備: 新規Expoプロジェクト

新しいExpoアプリを作成します。既存プロジェクトがあればそれを使って構いませんが、このチュートリアルでは新規プロジェクトを作ります。例ではnpmを使用しますが、任意のパッケージマネージャで進めてください。

npx create-expo-app@latest expo-custom-local-module-example --template blank-typescript

Expo Dev Client

カスタムネイティブコードを含むため、このプロジェクトはExpo Goでは動作しません。開発用ビルドを作成するためにexpo-dev-clientをインストールします。expo-dev-clientはデバッグツールを含むExpo Goに似たアプリですが、自分のネイティブモジュールが組み込まれます。

npx expo install expo-dev-client

カスタムモジュールのセットアップ

Expo Modules APIはJSI上の抽象レイヤーで、可能な限りプラットフォーム間で一貫したAPIを公開します。ローカルなExpo Moduleを作成していきます。

npx create-expo-module@latest --local

--localフラグは、そのモジュールをプロジェクト内に格納し、npmに公開する独立パッケージとしてではなくアプリ固有のモジュールとして作成します。プロジェクトルートにmodulesフォルダが作成されます。

想定されるフォルダ構成:

.
rest of your project root
modules/
└── expo-audio-route/
    ├── android/
    ├── ios/
    ├── src/
    ├── expo-module.config.js
    └── index.ts

ネイティブの実装はios/android/に入ります。これらはアプリのネイティブフォルダとは異なり、バージョン管理に含める必要があります(詳細は後述)。

生成ファイルのクリーンアップ

今回のモジュールは「ビュー」を公開する必要がないため、生成されたビュー関連ファイルを削除しておきます:

rm modules/expo-audio-route/src/ExpoAudioRouteView.tsx
rm modules/expo-audio-route/src/ExpoAudioRouteView.web.tsx
rm modules/expo-audio-route/src/ExpoAudioRouteModule.web.ts
rm modules/expo-audio-route/android/src/main/java/expo/modules/audioroute/ExpoAudioRouteView.kt
rm modules/expo-audio-route/ios/ExpoAudioRouteView.swift

API設計(JS側の契約)

ネイティブコードに入る前に、JavaScript側でどのAPIを公開するか決めます。ネイティブ側はこの契約を満たすように実装します。

このAudio Routeモジュールで欲しいものは次の2点です。

  • 現在のオーディオルートを取得する仕組み

    • JS側から1つの関数を呼び出すだけで、現在の出力先を知れるようにする。
    • 少なくとも次の値を扱う:
      • wiredHeadset
      • bluetooth
      • speaker
      • unknown(それ以外のフォールバック)
  • ルート変更を監視する仕組み

    • ヘッドホンを抜いたりBluetooth機器を接続したときに、ネイティブからJSへ通知できるようにする。

TypeScript型の定義

カスタムモジュール内のsrcディレクトリにあるExpoAudioRoute.types.tsExpoAudioRouteModule.tsを編集します。これらはネイティブとJSの間の契約(イベント名とペイロード、戻り値の型)を定義します。

パス:

modules/
└── expo-audio-route/
    ├── ios/
    ├── src/
    │   ├── ExpoAudioRoute.types.ts
    │   ├── ExpoAudioRouteModule.ts
    │   └── .. other files
    ├── expo-module.config.js
    └── index.ts

ExpoAudioRoute.types.tsの内容例:

export type ExpoAudioRouteModuleEvents = {
  onAudioRouteChange: (params: RouteChangeEvent) => void;
};

export type RouteChangeEvent = {
  route: AudioRoute;
};

export type AudioRoute =
  | "speaker"
  | "wiredHeadset"
  | "bluetooth"
  | "unknown";

続いて、ネイティブモジュールを型として公開するExpoAudioRouteModule.ts:

import { NativeModule, requireNativeModule } from "expo";
import { AudioRoute, ExpoAudioRouteModuleEvents } from "./ExpoAudioRoute.types";

declare class ExpoAudioRouteModule extends NativeModule<ExpoAudioRouteModuleEvents> {
  getCurrentRouteAsync(): Promise<AudioRoute>;
}

// This call loads the native module object from the JSI.
export default requireNativeModule<ExpoAudioRouteModule>("ExpoAudioRoute");

この定義により、JS側からはgetCurrentRouteAsync()を呼べ、イベント名とペイロードの型情報も得られます。

イベントの扱いについて

addListenerremoveAllListenersをTypeScript内で明示的に定義していなくても、NativeModule<TEvents>を継承することでExpoが自動的に以下のメソッドを提供します:

  • addListener(eventName, callback) - ネイティブ側のOnStartObservingブロックを最初のリスナー追加時に実行するトリガーになります。
  • removeAllListeners(eventName) または subscription.remove() - 最後のリスナーが削除されたときにネイティブ側のOnStopObservingを呼びます。

これにより、TypeScriptの型宣言(NativeModule<ExpoAudioRouteModuleEvents>)だけでイベント名とペイロード形状を型安全に扱えます。

index.tsの更新

ビューをエクスポートしていた箇所を削除し、モジュールだけをエクスポートするようにします:

export { default } from "./src/ExpoAudioRouteModule";
export * from "./src/ExpoAudioRoute.types";

ネイティブコードの実装(概要)

ios/android/ExpoAudioRouteModuleファイルを開き実装していきます。生成テンプレートにはビュー用のプレースホルダが入っていますが今回は不要です。

iOS用のテンプレート例(生成直後):

import ExpoModulesCore

public class ExpoAudioRouteModule: Module {
  public func definition() -> ModuleDefinition {
    Name("ExpoAudioRoute")
  }
}

Android用のテンプレート例(生成直後):

package expo.modules.audioroute

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class ExpoAudioRouteModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("ExpoAudioRoute")
  }
}

Expo Modulesでよく使うビルディングブロック

  • Events("onAudioRouteChange") - JSへ送るイベント名を宣言
  • AsyncFunction("getCurrentRouteAsync") - JSから呼べる非同期関数を定義
  • OnStartObserving("onAudioRouteChange") - 最初のリスナーが追加されたときに実行されるブロック
  • OnStopObserving("onAudioRouteChange") - 最後のリスナーが削除されたときに実行されるブロック

イベント名をパラメータとして渡すことで、複数イベントを持つモジュールでも各ブロックの意図が明確になります。


ネイティブ実装(パート1): 現在のオーディオルートを取得

まずは現在のルートを単純に問い合わせる関数から実装します。これはルート変更の監視を行わず、呼ばれた時に値を返すだけです。

インポート

iOSではAVFoundationAVAudioSessionを使って現在のオーディオルートにアクセスします。AndroidではAudioManagerで接続デバイスや状態を照会します。

iOS側のインポート例:

import ExpoModulesCore
import AVFoundation

Android側のインポート例:

package expo.modules.audioroute

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager

Android: AudioManagerの初期化

AndroidではAudioManagerの参照が必要です。モジュール内にprivateプロパティを追加し、OnCreateappContextから初期化します。

Android側の例(モジュール定義の中に追加):

class ExpoAudioRouteModule : Module() {
  private var audioManager: AudioManager? = null

  override fun definition() = ModuleDefinition {
    Name("ExpoAudioRoute")

    OnCreate {
      audioManager = appContext.reactContext?.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    }
  }
}

現在のルートを判定するヘルパー

両プラットフォームで、ネイティブの情報を正規化して次のいずれかの文字列を返すcurrentRoute()のようなヘルパーを実装します:

  • "wiredHeadset"
  • "bluetooth"
  • "speaker"
  • "unknown"

このヘルパーをAsyncFunction("getCurrentRouteAsync")でラップして、JSからExpoAudioRoute.getCurrentRouteAsync()として呼べるようにします。


ネイティブ実装(パート2): ルート変更を監視してイベントを送る

次に、ルート変更を監視してJSへイベントを送る実装を行います。Expo ModulesのEventsOnStartObservingOnStopObservingを使います。

  • iOS: AVAudioSession.routeChangeNotificationを監視し、変更時にonAudioRouteChangeイベントを送る。
  • Android: AudioDeviceCallbackを実装してシステムに登録し、変更時にonAudioRouteChangeイベントを送る。

OnStartObserving("onAudioRouteChange")でネイティブの監視を開始し、OnStopObserving("onAudioRouteChange")で監視を解除します。これにより、JS側のリスナー数に応じてネイティブ側の監視を効率的に管理できます。


テストと配布

  • オーディオルートのテストは実機で行ってください。シミュレータ/エミュレータではテストが難しい場合が多いです。
  • 開発中はexpo-dev-clientを使った開発ビルドでネイティブモジュールを組み込んだアプリを動かします。
  • ios/android/内のネイティブコードはプロジェクト固有の実装であるため、バージョン管理に含めることを推奨します。
  • モジュールをアプリ専用に保つ場合は--localで作成したままにし、他アプリでも使いたければパッケージとして切り出してnpmに公開できます。

参考

  • 深掘りしたい場合はExpo Modules APIのドキュメントを参照してください(公式ドキュメントを参照)。

このチュートリアルの要点は、JS側で明確な契約(関数とイベント)を設計し、それを満たす形でiOS/AndroidのネイティブAPIをラップすることです。Expo Modulesはそのためのビルディングブロックを提供しており、開発者は同じ設計方針で両プラットフォームに対応できます。