ClaudeExpoOct 16, 2025, 4:15 PM

How to add native code to your app with Expo Modules

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

claudeenmodel: claude-sonnet-4-20250514

How to Add Native Code to Your App with Expo Modules

Key Points

  • Build custom native modules with Expo Modules API
  • Create audio route detection with cross-platform support
  • Use TypeScript-first API design for native functionality

Summary

This tutorial demonstrates how to create custom native functionality in React Native apps using Expo Modules API. The guide walks through building an audio route detector module that can identify where device audio is being output (speaker, headphones, bluetooth) and listen for route changes.

Key Points

  • Setup Process: Create a local Expo module using npx create-expo-module@latest --local which generates platform-specific folders for iOS and Android native code
  • API Design: Define TypeScript interfaces first to establish the contract between native code and JavaScript, including event types and function signatures
  • Native Implementation: Use Expo Modules API building blocks like AsyncFunction, Events, OnStartObserving, and OnStopObserving to bridge native audio APIs
  • Platform Integration: Import AVFoundation on iOS and AudioManager/AudioDeviceInfo on Android to access native audio routing functionality
  • Development Build Required: Custom native modules require expo-dev-client instead of Expo Go for testing and development
  • Event System: Automatic listener management with addListener() and removeAllListeners() methods provided by extending NativeModule

Full Translation

Translations

A translation section that keeps the flow of the original article.

claudejamodel: claude-sonnet-4-20250514

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

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 --template blank-typescript

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

--localフラグは、npmに公開できるスタンドアロンパッケージではなく、プロジェクト内に存在するモジュールを作成します。特定のアプリにカスタムネイティブ機能が必要な場合に最適です。

これにより、プロジェクトのルートにmodulesフォルダが作成されます。

..
. プロジェクトルートの残り
modules/
└── expo-audio-route/
    ├── android/
    ├── ios/
    ├── src/
    ├── expo-module.config.js
    └── index.ts

カスタムネイティブコードはiosandroidディレクトリ内に配置されます。アプリのネイティブフォルダとは異なり、これらはバージョン管理する必要があります(詳細は後述)。

クリーンアップ

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.tsExpoAudioRouteModuleの内容をクリアすることから始めましょう。これらはカスタムモジュールの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>;
}

// この呼び出しはJSIからネイティブモジュールオブジェクトを読み込みます。
export default requireNativeModule<ExpoAudioRouteModule>("ExpoAudioRoute");

現在、JavaScript APIには2つの主要なエントリーポイントがあります:

  • getCurrentRouteAsync() - 現在のオーディオルートがネイティブ側で何であるかを問い合わせるためにいつでも呼び出せる関数
  • ネイティブ側がルート変更を検出したときにイベントを購読する方法

TypeScriptファイルでaddListenerremoveListener関数を定義していないことに気づくかもしれません。これは、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つの文字列値のいずれかに正規化します:wiredHeadsetbluetooth