Expo modulesを使ったReact Nativeのネイティブ拡張入門

この記事は[ソニックガーデン プログラマ アドベントカレンダー](https://qiita.com/advent-calendar/2024/sonicgarden)の5日目の記事です。

---

# React Nativeの近況

React Nativeは、Flutterの勢いに押されていますが、ネイティブアプリを実装する上でおすすめなフレームワークだと考えています。

* ウェブの技術で書けるため、扱いやすい人が多い
* クロスプラットフォームにも対応可能
* ネイティブの拡張も難易度が低い（この記事で主に触れたい点です）

React Nativeが難しいと感じられることの一つに、アップグレードの手間があります。
以前のバージョンのアップデートは確かに大変でしたが、フレームワークとしての改善が進んでおり、アップグレードの手間は減っています。
Expoを使うことで、ネイティブ層のコードを管理する必要がなくなっています。

そもそも、アップグレードの手間は、フレームワークの問題というよりも、多くの小さなライブラリを組み合わせて利用しているため、それらの管理の手間が大きな要因です。
特にnpmの界隈では、多くの小さなライブラリを組み合わせることで問題を解決する方向性が強く、複雑性が増しやすいです。
ライブラリの管理の問題は、React Nativeだけの問題ではなく、Flutterでも同様のことが起こり得ると思います。

また、ネイティブでの実装との比較としては、フレームワークでサポートされていないネイティブ層の機能を利用することの難しさがありました。
この部分まで容易に利用できるようになると、スマホアプリを実装する上でのフレームワークとしての優位性は高いと考えます。

# modules について

この記事では、Expoを使って比較的簡単に独自のネイティブコードを実行できる `modules` の機能を紹介します。
非常にネイティブの拡張を実装しやすくなっており、その優位性を感じていただきたいです。

基本的には、ドキュメントを参考に生成されるコードを読めば要点は把握できるようなサンプルコードを生成してくれます。
ここでは、簡単にフィボナッチ数列を返す関数をネイティブ側で実装して返すサンプルを作ってみたいと思います。

# modules のサンプルを作成

まず、以下のコマンドでExpoでのアプリを作成します。

```bash
npx create-expo-app@latest
> fib-modules-app
```

次に、ディレクトリ内で `modules` を生成します。`--local` オプションを指定することでアプリレポジトリ内にmodulesを含む形にします。

```bash
npx create-expo-module@latest --local
> fib-module
```

`--local` なしで実行することで、共通ライブラリとしても使用することが可能です。

生成されるコードの構造は以下の通りです。

```text
$ tree modules
modules
└── fib-module
    ├── android
    │   ├── build.gradle
    │   └── src
    │       └── main
    │           ├── AndroidManifest.xml
    │           └── java
    │               └── expo
    │                   └── modules
    │                       └── fibmodule
    │                           ├── FibModule.kt
    │                           └── FibModuleView.kt
    ├── expo-module.config.json
    ├── index.ts
    ├── ios
    │   ├── FibModule.podspec
    │   ├── FibModule.swift
    │   └── FibModuleView.swift
    └── src
        ├── FibModule.ts
        ├── FibModule.types.ts
        ├── FibModule.web.ts
        ├── FibModuleView.tsx
        └── FibModuleView.web.tsx

11 directories, 14 files
```

iOSに絞って重要なファイルをみると2つのファイルがあります。

### src/FibModule.ts

サンプルに以下をReact Native 側から呼び出すための定義しています。
* 定数として `PI`
* 関数として `hello`
* 非同期の関数として `setValueAsync` 

```typescript
import { NativeModule, requireNativeModule } from 'expo';

import { FibModuleEvents } from './FibModule.types';

declare class FibModule extends NativeModule<FibModuleEvents> {
  PI: number;
  hello(): string;
  setValueAsync(value: string): Promise<void>;
}

// This call loads the native module object from the JSI.
export default requireNativeModule<FibModule>('FibModule');
```

### ios/FibModule.swift

swift での関数の中身の実装です。
簡単なサンプルなので、生成されたコードを読んでもらえれば容易にネイティブでの拡張ができることを感じてもらえると思います。

```swift
import ExpoModulesCore

public class FibModule: Module {
  // Each module class must implement the definition function. The definition consists of components
  // that describes the module's functionality and behavior.
  // See https://docs.expo.dev/modules/module-api for more details about available components.
  public func definition() -> ModuleDefinition {
    // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
    // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
    // The module will be accessible from `requireNativeModule('FibModule')` in JavaScript.
    Name("FibModule")

    // Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
    Constants([
      "PI": Double.pi
    ])

    // Defines event names that the module can send to JavaScript.
    Events("onChange")

    // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
    Function("hello") {
      return "Hello world! 👋"
    }

    // Defines a JavaScript function that always returns a Promise and whose native code
    // is by default dispatched on the different thread than the JavaScript runtime runs on.
    AsyncFunction("setValueAsync") { (value: String) in
      // Send an event to JavaScript.
      self.sendEvent("onChange", [
        "value": value
      ])
    }

    // Enables the module to be used as a native view. Definition components that are accepted as part of the
    // view definition: Prop, Events.
    View(FibModuleView.self) {
      // Defines a setter for the `url` prop.
      Prop("url") { (view: FibModuleView, url: URL) in
        if view.webView.url != url {
          view.webView.load(URLRequest(url: url))
        }
      }

      Events("onLoad")
    }
  }
}
```

この後に、ネイティブの拡張が入るので、以下のコマンドでシミュレータを起動し、ネイティブの拡張を含んだ形のアプリをビルドできます。

```bash
npx expo prebuild
npx expo run:ios
```

## 独自関数を定義する

最後に、フィボナッチ数列をネイティブで処理するコードサンプルを置いておきます。

### src/FibModule.ts

```diff 
 declare class FibModule extends NativeModule<FibModuleEvents> {
   PI: number;
   hello(): string;
   setValueAsync(value: string): Promise<void>;
+  fibonacci(num: number): number[];
 }
 ```

### ios/FibModule.swift

```diff
       return "Hello world! 👋"
     }
 
+    Function("fibonacci") { (num: Int) in
+      assert(num > 1)
+
+      var array = [0, 1]
+
+      while array.count < num {
+        array.append(array[array.count - 1] + array[array.count - 2])
+      }
+
+      return array
+    }
+
     // Defines a JavaScript function that always returns a Promise and whose native code
```

（swift の実装については、僕がよく書く言語では無いので稚拙かと思いますがご容赦をお願いします）

これだけの実装で処理をネイティブに渡すことが可能になります。
React Native でネイティブの拡張を作ることの簡単さは感じてもらえるのでないでしょうか。

# まとめ

最近は少し人気が薄れてきている印象があるReact Nativeですが、フレームワークが成熟することでクロスプラットフォームのアプリを効率よく、ネイティブ層の拡張も含めてスムーズに作れるようになっています。
ぜひ、スマホアプリの実装の際には検討してみてください。

---

[ソニックガーデン プログラマ アドベントカレンダー](https://qiita.com/advent-calendar/2024/sonicgarden) 6日目は @maedana です。お楽しみに！