Expo Modulesを使ってアプリにネイティブコードを追加する方法
Development • React Native • Product • October 16, 2025 • 17分で読める
Jacob Clausen Engineering
Expo Modulesを使ってReact Nativeにネイティブの力を追加しましょう。このチュートリアルでは、オーディオルート検出モジュールを構築し、イベントリスナーをカバーし、テストと配布の方法を示します。
Expoエコシステムと寛大なコミュニティは、アプリのほぼすべてのニーズに対応するライブラリを提供しています。しかし、時にはネイティブ機能が必要で、利用可能なオプションが私たちのユースケースに合わない場合があります。そのような時に、カスタムネイティブコードの作成が不可欠になります。
Expo Modulesを使用すると、Expoの利点を享受しながら、カスタムネイティブ機能を作成できます。Expo SDKに付属するものに縛られることはありません。要件に合わせてアプリを拡張する柔軟性があります。
この投稿では、オーディオルートを検出するためのカスタムネイティブモジュールの作成方法を見ていきます。オーディオルートとは、デバイスが現在音声出力を送信している場所(例:スピーカー、ヘッドフォン、Bluetoothデバイス)のことです。
注意: シミュレーターやエミュレーターでオーディオルートの変更をテストする方法は非常に限られているため、実際のデバイスで試すことに焦点を当てることをお勧めします。
新しいExpoプロジェクト
新しいExpoアプリを作成することから始めます。既存のプロジェクトがある場合はそれを使用することもできますが、このチュートリアルでは新しいものを作成します。この例ではnpmを使用しますが、お好みのパッケージマネージャーで自由に進めてください。
npx create-expo-app@latest expo-custom-local-module-example
Expo Dev Client
このプロジェクトにはカスタムネイティブコードが含まれるため、Expo Go内では実行できません。そのため、開発ビルドを作成することから始めます。expo-dev-clientパッケージは、Expo Goに似たアプリを提供しますが、デバッグツールが完備され、独自のネイティブモジュールが含まれています。
npx expo install expo-dev-client
カスタムモジュールのセットアップ
Expo Modules APIは、JSIの上の抽象化レイヤーで、可能な限りプラットフォーム間で一貫したAPIを便利に公開します。基本的なセットアップは完了しました。ローカルExpo Moduleの作成を始めましょう。
名前を求められたらexpo-audio-routeを使用し、その後のプロンプトでは提案を受け入れます。
npx create-expo-module@latest
--localフラグは、npmに公開できるスタンドアロンパッケージではなく、プロジェクト内に存在するモジュールを作成します。特定のアプリにカスタムネイティブ機能が必要な場合に最適です。
これにより、プロジェクトのルートにmodulesフォルダが作成されます。
..
. プロジェクトルートの残り
modules/
└── expo-audio-route/
├── android/
├── ios/
├── src/
├── expo-module.config.js
└── index.ts
カスタムネイティブコードはiosとandroidディレクトリ内に配置されます。アプリのネイティブフォルダとは異なり、これらはバージョン管理する必要があります(詳細は後述)。
クリーンアップ
Expo Modulesは、ネイティブ機能を公開したり、カスタムネイティブビューを作成したりするために使用できます。今回は機能のみが必要なので、不要な生成されたビューファイルを安全に削除できます。
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の計画
SwiftやKotlinに触れる前に、JavaScript APIがどのようなものであるべきかを決定します。これが公開され、開発者が実際に使用するものです。ネイティブ側で行うすべてのことは、この契約に奉仕するものです。
Audio Routeモジュールでは、2つのことが必要です:
現在のオーディオルートを問い合わせる方法
開発者は1つの関数を呼び出して、オーディオが以下のどこで再生されているかを知ることができるべきです:
- wiredHeadset
- bluetooth
- speaker
- unknown(その他のフォールバック)
カバーすべき選択肢は他にもありますが、今回はこれらで十分です。
変更をリッスンする方法
オーディオルートはいつでも変更される可能性があります:ヘッドフォンを抜いたり、Bluetoothデバイスを接続したりする場合です。モジュールは、これが発生したときにJavaScriptに通知する必要があります。
TypeScript型の設定
ExpoAudioRoute.types.tsとExpoAudioRouteModuleの内容をクリアすることから始めましょう。これらはカスタムモジュールのsrcディレクトリにあります。
modules/
└── expo-audio-route/
├── ios/
├── src/
│ ├── ExpoAudioRoute.types.ts
│ ├── ExpoAudioRouteModule.ts
│ └── .. その他のファイル
├── expo-module.config.js
└── index.ts
次に、TypeScript型を定義します。これらは、可能なオーディオルート、イベントペイロード、およびモジュールが発行するイベントの形状を記述します。これらの定義を配置することで、ネイティブコードとJavaScript側の間の明確な契約が得られます。
modules/expo-audio-route/src/ExpoAudioRoute.types.ts
export type ExpoAudioRouteModuleEvents = {
onAudioRouteChange: (params: RouteChangeEvent) => void;
};
export type RouteChangeEvent = {
route: AudioRoute;
};
export type AudioRoute =
| "speaker"
| "wiredHeadset"
| "bluetooth"
| "unknown";
次に、これらの型にネイティブモジュールを接続することで、計画した機能を公開します。このステップにより、開発者がアプリから直接getCurrentRouteAsync()を呼び出し、onAudioRouteChangeイベントを購読できるように、JavaScript で契約を利用可能にします。
modules/expo-audio-route/src/ExpoAudioRouteModule.ts
import { NativeModule, requireNativeModule } from "expo";
import { AudioRoute, ExpoAudioRouteModuleEvents } from "./ExpoAudioRoute.types";
declare class ExpoAudioRouteModule extends NativeModule<ExpoAudioRouteModuleEvents> {
getCurrentRouteAsync(): Promise<AudioRoute>;
}
export default requireNativeModule<ExpoAudioRouteModule>("ExpoAudioRoute");
現在、JavaScript APIには2つの主要なエントリーポイントがあります:
getCurrentRouteAsync() - 現在のオーディオルートがネイティブ側で何であるかを問い合わせるためにいつでも呼び出せる関数
- ネイティブ側がルート変更を検出したときにイベントを購読する方法
TypeScriptファイルでaddListenerやremoveListener関数を定義していないことに気づくかもしれません。これは、ExpoのNativeModule型から拡張するときに、これらが既に組み込まれているためです。
Expo Modulesは、イベントを宣言するすべてのネイティブモジュールで以下のメソッドを自動的に公開します:
addListener(eventName, callback) - ネイティブからのイベントのリッスンを開始します。内部的には、リスナーが初めて追加されたときにSwift/KotlinのOnStartObservingブロックをトリガーします。
removeAllListeners(eventName)またはsubscription.remove() - イベントリスナーを削除します。最後のリスナーが削除されると、Expoはネイティブ側でOnStopObservingブロックを呼び出します。
これは、TypeScript宣言(NativeModule<ExpoAudioRouteModuleEvents>)がイベント名とペイロードの形状をTypeScriptに認識させるのに十分であることを意味します。
最後に、前のステップでViewsに関連するファイルを既に削除したため、ExpoAudioRouteViewのエクスポートを停止するようにモジュールのindex.tsファイルを更新します。
modules/expo-audio-route/index.ts
export { default } from "./src/ExpoAudioRouteModule";
export * from "./src/ExpoAudioRoute.types";
ネイティブコードの開始
ios/とandroid/フォルダの両方でExpoAudioRouteModuleファイルを開いて、モジュールのネイティブ側に飛び込みます。create-expo-moduleによって生成されたテンプレートには、必要のないビューやその他の機能のプレースホルダーコードが含まれています。
modules/expo-audio-route/ios/ExpoAudioRouteModule.swift
import ExpoModulesCore
public class ExpoAudioRouteModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoAudioRoute")
}
}
modules/expo-audio-route/android/src/main/java/expo/modules/audioroute/ExpoAudioRouteModule.kt
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")
}
}
目標(現在のルートの問い合わせ、変更のリッスン、シンプルなAPIの公開)を達成するために、Expo Modules APIのいくつかの構成要素を使用して、Audio RouteモジュールをiOSとAndroidの基盤となるオーディオAPIに接続します:
- Events - モジュールがJavaScriptに送信できるイベント名を宣言します。例:
Events("onAudioRouteChange")。複数の例:Events("onAudioRouteChange", "onSomethingElseChanged")。
- AsyncFunction - JavaScriptが呼び出せるネイティブ関数を定義します。例:
AsyncFunction("getCurrentRouteAsync") - JSでAudioRoute.getCurrentRouteAsync()として使用します。
- OnStartObserving - 最初のJSリスナーが
.addListenerで追加されたときに自動的に実行されます。
- OnStopObserving - 最後のJSリスナーが削除されたときに自動的に実行されます。
これらを宣言するときは、イベント名をパラメータとして渡すことが推奨されます。例:OnStartObserving("onAudioRouteChange")とOnStopObserving("onAudioRouteChange")。これにより意図が明確になり、各ブロックが処理するイベントに直接対応することが保証されます。これは、モジュールが複数のイベントタイプを発行する場合に特に役立ちます。
次のコードセクションでは、これらのそれぞれを実装する方法を見ていきます。Expo Modulesは、モジュールができることを記述するための多くの構成要素を提供します。より深く掘り下げたい場合は、Expo Modules APIドキュメントをチェックしてください。
以下は、アプリが使用されるときにこれらの構成要素が動作する様子を示すクイック録画です。
Xcodeでブレークポイントを設定して、どのメソッドがいつ実行されるかを示す
最初のステップとして、アプリで現在のオーディオルートを取得することに焦点を当てます。この関数は、変更をリッスンすることなく、要求されたときにルートを返すだけです。
ネイティブコード パート1 - オーディオルートの取得
インポート
各プラットフォームで必要なネイティブオーディオAPIを追加することから始めます。
iOSでは、AVFoundationがAVAudioSessionを通じてデバイスの現在のオーディオルートへのアクセスを提供します。
Androidでは、接続されたデバイスを問い合わせるためのAudioManager、それらを記述するためのAudioDeviceInfo、およびルート変更をリッスンするためのAudioDeviceCallback(後で使用)をインポートします。
modules/expo-audio-route/ios/ExpoAudioRouteModule.swift
import ExpoModulesCore
+ import AVFoundation
modules/expo-audio-route/android/src/main/java/expo/modules/audioroute/ExpoAudioRouteModule.kt
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
AudioManagerの初期化(Androidのみ)
Androidでは、後でオーディオルートの変更を問い合わせたりリッスンしたりできるように、システムのAudioManagerへの参照が必要です。モジュール内にプライベートプロパティを追加し、Expo Modulesによって提供されるappContextを使用してOnCreateブロックで初期化します。これにより、オーディオ関連の操作に対して有効なAudioManagerインスタンスが準備されます。
modules/expo-audio-route/android/src/main/java/expo/modules/audioroute/ExpoAudioRouteModule.kt
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()という名前のプライベートヘルパーを定義します。両プラットフォームで、このメソッドはネイティブオーディオ情報を4つの文字列値のいずれかに正規化します:wiredHeadset、bluetooth