📱

React Nativeでネイティブコードを使う (Expo Modules API編)

2024/12/16に公開

この記事はReact Native 全部俺 Advent Calendar 16目の記事です。

https://adventar.org/calendars/10741

このアドベントカレンダーについて

このアドベントカレンダーは @itome が全て書いています。

基本的にReact NativeおよびExpoの公式ドキュメントとソースコードを参照しながら書いていきます。誤植や編集依頼はXにお願いします。

React Nativeでネイティブコードを使う (Expo Modules API編)

React Nativeで開発をしていると、ほとんどの場合はJavaScriptを書くだけで済みます。しかし、アプリ開発ではプッシュ通知やカメラ、GPSなど、プラットフォーム固有の機能を使う必要が出てきます。

今回はExpo Modules APIを使ってネイティブコード(AndroidのKotlinとiOSのSwift)を書く方法を紹介します。

なぜExpo Modules APIを使うのか

React Nativeでネイティブコードを書く方法は、従来から以下の2つがありました:

  1. React Native Bridge APIを使う方法
  2. React Native Turbo Modules APIを使う方法

しかし、これらのAPIには以下のような問題がありました:

  • JavaとObjective-Cで書く必要がある(Turbo ModulesはSwiftもサポート)
  • 型安全性が低く、コード補完が効きづらい
  • ネイティブプロジェクトの設定が煩雑
  • マルチプラットフォーム対応が難しい

Expo Modules APIはこれらの問題を解決するために作られた新しいフレームワークです。

混乱しやすい用語の整理

Expoでネイティブコードを伴う開発をしたいときに、「Expo ネイティブコード」などと調べると、CNG、Expo Prebuild、Turbo Modules APIなど似たような概念が多く出てきて分かりづらいと思います。これらの違いは混乱しやすいですが、ざっくり以下のような理解をしておくといいです。

  • Expo Prebuild: 設定ファイルやネイティブコードを含むライブラリからネイティブアプリのプロジェクトを生成するための機能。
  • CNG(Continuous Native Genration): Expo Prebuildを他のフレームワークにも応用できるように抽象化した概念
  • Turbo Modules API: React Native公式のネイティブモジュールを作るためのAPI。JSIを使うため高速に動作するが、New Architectureに依存しているため新しいバージョンのReact Nativeが必要。
  • Expo Modules API: Expo公式のネイティブモジュールを作るためのAPI。Turbo Modules APIと同等のパフォーマンスで動作し、New Architecture以前のバージョンでも動作するが、Expoに依存しているためExpoを導入しているプロジェクトでないと利用できない。

Expo Modules APIの特徴

1. モダンな言語でネイティブコードが書ける

AndroidではKotlin、iOSではSwiftを使ってネイティブコードが書けます。どちらも型安全な言語で、IDEのコード補完も効くため、開発効率が大幅に向上します。

// AndroidのKotlinコード例
class MyModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("MyModule")
    
    Function("hello") { name: String ->
      "Hello, $name!"
    }
  }
}
// iOSのSwiftコード例
class MyModule: Module {
  func definition() -> ModuleDefinition {
    Name("MyModule")
    
    Function("hello") { (name: String) -> String in
      return "Hello, \(name)!"
    }
  }
}

従来のReact Native Bridge APIと比べると、コードがシンプルで読みやすくなっています。また、型情報がTypeScriptの型定義にも反映されるため、JavaScriptからの呼び出しも安全に行えます。

2. autoLinkingの仕組み

Expo Modules APIは、autoLinkingと呼ばれる仕組みを持っています。これにより、ネイティブプロジェクトの設定が大幅に簡略化されます。

以前は、AndroidのGradleファイルやiOSのPodfileを手動で編集する必要がありましたが、Expo Modules APIではnpmパッケージをインストールするだけで自動的に設定が行われます。

{
  "platforms": ["ios", "android"],
  "ios": {
    "modules": ["MyModule"]
  },
  "android": {
    "modules": ["expo.modules.mymodule.MyModule"]
  }
}

このように設定ファイルを用意しておくだけで、必要なネイティブモジュールが自動的にリンクされます。

3. マルチプラットフォーム対応が簡単

Expo Modules APIの大きな特徴の1つが、macOSやtvOSなどのプラットフォームにも対応できることです。

{
  "platforms": ["apple"],
  "apple": {
    "modules": ["MyModule"]
  }
}

上記のようにplatforms"apple"を指定することで、iOS/macOS/tvOSのすべてのAppleプラットフォームに対応できます。

モジュールの基本的な実装方法

それでは実際にExpo Modules APIを使ってモジュールを実装してみましょう。まずはプロジェクトの初期化から始めます。

# モジュールの雛形を作成
npx create-expo-module my-module

このコマンドを実行すると、以下のようなプロジェクト構造が生成されます:

my-module/
  ├── android/                # Androidのネイティブコード
  ├── ios/                   # iOSのネイティブコード
  ├── src/                   # TypeScriptのコード
  ├── example/               # サンプルアプリ
  └── expo-module.config.json # モジュールの設定ファイル

プラットフォーム固有の機能の実装

Expo Modules APIの重要な機能の1つが、プラットフォーム固有の機能を簡単に実装できることです。

Android Lifecycle Listeners

Androidでは、アクティビティのライフサイクルイベントをフックする仕組みを提供しています。これはMainActivityやMainApplicationに直接コードを書く必要がなく、モジュール内で完結して実装できます。

class MyLibPackage : Package {
  override fun createReactActivityLifecycleListeners(activityContext: Context): List<ReactActivityLifecycleListener> {
    return listOf(MyLibReactActivityLifecycleListener())
  }
}

class MyLibReactActivityLifecycleListener : ReactActivityLifecycleListener {
  override fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
    // ActivityのonCreateで実行したい処理
    doSomeSetupInActivityOnCreate(activity)
  }
}

以下のようなライフサイクルメソッドをオーバーライドできます:

  • onCreate
  • onResume
  • onPause
  • onDestroy
  • onNewIntent
  • onBackPressed

iOS AppDelegate Subscribers

iOS側でも同様に、AppDelegateのメソッドをフックする仕組みが用意されています。ExpoAppDelegateSubscriberを継承したクラスを作成し、モジュールのコンフィグに登録するだけです。

public class AppLifecycleDelegate: ExpoAppDelegateSubscriber {
  public func applicationDidBecomeActive(_ application: UIApplication) {
    // アプリがアクティブになったときの処理 
  }

  public func applicationWillResignActive(_ application: UIApplication) {
    // アプリが非アクティブになるときの処理
  }

  public func applicationDidEnterBackground(_ application: UIApplication) {
    // バックグラウンドに移行したときの処理
  }
}

イベントの送受信

ネイティブからJavaScriptにイベントを送信する仕組みも用意されています。まずmodule定義でイベント名を登録し、sendEvent()メソッドでペイロードと共にイベントを発火できます。

// Androidの場合
override fun definition() = ModuleDefinition {
  Events("onClipboardChanged")
  
  // イベント送信
  sendEvent("onClipboardChanged", mapOf(
    "contentTypes" to availableContentTypes()
  ))
}
// iOSの場合
public func definition() -> ModuleDefinition {
  Events("onClipboardChanged")
  
  // イベント送信
  sendEvent("onClipboardChanged", [
    "contentTypes": availableContentTypes()
  ])
}

JavaScript側ではaddListener()で受信できます:

const subscription = ExpoClipboardModule.addListener(
  'onClipboardChanged', 
  (event) => console.log(event.contentTypes)
);

// 不要になったら削除
subscription.remove();

まとめ

Expo Modules APIは以下のような特徴と利点を持ちます:

  1. モダンな言語でネイティブ開発ができる

    • AndroidはKotlin、iOSはSwiftを使用可能
    • 型安全で開発体験が良い
  2. autoLinkingによる自動設定

    • ネイティブプロジェクトの設定が自動化される
    • 開発者の手間を大幅に削減
  3. マルチプラットフォーム対応が簡単

    • iOS/macOS/tvOSに一括対応可能
    • プラットフォーム固有の機能も統一的なAPIで実装可能
  4. プラフォーム固有の機能をうまく抽象化

    • ライフサイクルイベントのフック
    • イベントの送受信
    • ViewやControllerの管理

もしReact Nativeアプリでネイティブの機能を使いたい場合、Expo Modules APIは非常に良い選択肢となります。

Discussion