ReactNativeでAndroidのネイティブモジュールを実装する

作りたいアプリがAndroidのネイティブモジュールが必要なものだったので、作ることにしました。
2年前にもちょっとさわりましたが、前回はだいぶ適当だった＋忘れていることもあり、自分用のメモも兼ねて書きます。

# はじめに

ReactNativeにはデフォルトである程度のネイティブモジュールが用意されています。例えば、カメラや録音など

通常その用意されたモジュールを使うので、Androidのネイティブモジュールを別段実装する必要はありません。

用意されたモジュールでは対応できない場合に実装する必要があります。

実装はAndroidJavaで行います。


# Androidのネイティブモジュール

ドキュメントは[ここ](https://reactnative.dev/docs/native-modules-android)を読みました。

今回はドキュメントに書かれているToastModule（一瞬表示されるアラートのようなやつ）を参考に勘所が書けたらなと思います。

## モジュールを作る（例.ToastModule.java）
### ①作りたいモジュールのJavaファイルを作る
作る場所はandroid/app/src/main/hjava/com/{{アプリ名}}/

```
{{作りたいモジュール名}}Module.java
　例：ToastModule.java
```

というファイルを用意します。
ここで作ったモジュールが、ReactNativeから呼び出すモジュールになります。

### ②お作法にのっとってモジュールを作る

作ったjavaファイルに、モジュールを書きます。

```ToastModule.java
package com.your-app-name;

// AndroidのToastパッケージ
import android.widget.Toast;

// ReactNativeのパッケージ使用
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

// Javaのパッケージ
import java.util.Map;
import java.util.HashMap;

// ToastModule本体
public class ToastModule extends ReactContextBaseJavaModule {
  private static ReactApplicationContext reactContext;

  // Toastの表示時間を決める変数
  private static final String DURATION_SHORT_KEY = "SHORT";
  private static final String DURATION_LONG_KEY = "LONG";

  // ReactNativeのパッケージのクラスを継承。必須
  ToastModule(ReactApplicationContext context) {
    super(context);
    reactContext = context;
  }

  // モジュール名。JSから呼び出す際の名前。必須
  @Override
  public String getName() {
    return "ToastExample";
  }

  // 定数定義。ReactNative側の定数を返します
  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
    constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
    return constants;
  }

  // ReactNative側が呼び出すメソッド。実現したい機能を実装します。
  // ToastModuleでは、AndroidのToastモジュールを呼び出し、
  // メッセージを指定された秒数だけ表示するメソッドを作っています。
  @ReactMethod
  public void show(String message, int duration) {
    Toast.makeText(getReactApplicationContext(), message, duration).show();
  }
}
```

ReactNative側からReactMethodに変数を渡すと下記のようにマッピングされます。

```
JavaScript -> Java

Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array
```

contextの継承部分とget_name（モジュールの名前を定義）の部分以外は好きに書いて大丈夫のようです。

## モジュールのパッケージ化（例.CustomToastPackage.java）

作ったモジュールをパッケージ化します。

### ①パッケージのJavaファイルを作る
作る場所はandroid/app/src/main/hjava/com/{{アプリ名}}/

```
{{作りたいパッケージ名}}Package.java
　例：CustomToastPackage.java
```

というファイルを用意します。
このパッケージファイルにモジュール（複数可）を登録し、MainApplication.javaから読みこんで使います。


### ②お作法にのっとって書く


```CustomToastPackage.java
package com.your-app-name;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CustomToastPackage implements ReactPackage {

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  @Override
  public List<NativeModule> createNativeModules(
                              ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();

    modules.add(new ToastModule(reactContext));

    return modules;
  }

}
```

一瞬難しく感じますが、このファイルでやっているのは、モジュールをパッケージ化しているだけです。

```java
modules.add(new ToastModule(reactContext));
```

この部分で作ったモジュールを追加しています。
そのため、別のモジュールを追加する場合は、

```java
modules.add(new ToastModule(reactContext));
modules.add(new ToastModule2(reactContext));
```

のようにするだけです。


## パッケージをMainApplicationに登録

###①パッケージを登録し、ReactNativeのNativeModuleから呼び出せるようにする
ReactNativeではNativeModuleというパッケージが用意されており、このパッケージからNativeモジュールを呼び出します。
NativeModuleから呼び出すために、MainApplication.javaにパッケージを登録します。

ファイルの場所はandroid/app/src/main/hjava/com/{{アプリ名}}/MainApplication.java
このファイルはデフォルトで用意されていると思います。ここに

```MainApplication.java
...
// 作ったパッケージ
import com.your-app-name.CustomToastPackage; // <-- Add this line with your package name.
...

protected List<ReactPackage> getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
  // 作ったパッケージを登録
  packages.add(new CustomToastPackage()); // <-- Add this line with your package name.
  return packages;
}
```

のように書くことで、パッケージを呼び出せるようになります。

## JSから呼び出す

最後に、JSからモジュールを呼び出す専用のファイルを呼び出すコードを書きます。

```ToastExample.js
import { NativeModules } from 'react-native';
// get_nameで指定した名前
module.exports = NativeModules.ToastExample;
```

```App.js
import ToastExample from './ToastExample';
// Module.javaでReactMethodで定義したメソッド
ToastExample.show('Awesome', ToastExample.SHORT);
```


以上、ネイティブモジュールの作り方です。

ここからちょっとおまけになります。
ネイティブモジュールのメソッド（ReactMethodの指定がされているメソッド）で値を返したい場合、どういう返しをするのかという話があります。


# ネイティブモジュールの戻り値

ネイティブモジュールからは指定の値をコールバックとPromiseで返せます。
ブリッジ通信は非同期であり、呼び出しと値の受け取りはちょっと考える必要があります。

例えば、コールバックの場合

```java
import com.facebook.react.bridge.Callback;

public class UIManagerModule extends ReactContextBaseJavaModule {

...

  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Callback errorCallback,
      Callback successCallback) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);
      float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
      float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
      float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
      float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
      successCallback.invoke(relativeX, relativeY, width, height);
    } catch (IllegalViewOperationException e) {
      errorCallback.invoke(e.getMessage());
    }
  }

...
```

上のコードの場合、
成功したら　xy座標と横幅と縦幅を返す
失敗したら　エラーメッセージを返す
という感じです。JS側では、

```javascript
UIManager.measureLayout(
  100,
  100,
  (msg) => {
    console.log(msg);
  },
  (x, y, width, height) => {
    console.log(x + ':' + y + ':' + width + ':' + height);
  }
);
```

のように書くことで、成功と失敗両方に対応できます。

また、Promiseの場合は、

```java
import com.facebook.react.bridge.Promise;

public class UIManagerModule extends ReactContextBaseJavaModule {

...
  private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR";
  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Promise promise) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);

      WritableMap map = Arguments.createMap();

      map.putDouble("relativeX", PixelUtil.toDIPFromPixel(mMeasureBuffer[0]));
      map.putDouble("relativeY", PixelUtil.toDIPFromPixel(mMeasureBuffer[1]));
      map.putDouble("width", PixelUtil.toDIPFromPixel(mMeasureBuffer[2]));
      map.putDouble("height", PixelUtil.toDIPFromPixel(mMeasureBuffer[3]));

      promise.resolve(map);
    } catch (IllegalViewOperationException e) {
      promise.reject(E_LAYOUT_ERROR, e);
    }
  }

...
```

上のコードの場合、
成功したら　xy座標と横幅と縦幅を持つJSオブジェクトを返す
失敗したら　エラーオブジェクトを返す
という感じです。JS側では、

```javascript
const measureLayout = async () => {
  try {
    var {
      relativeX,
      relativeY,
      width,
      height
    } = await UIManager.measureLayout(100, 100);

    console.log(
      relativeX + ':' + relativeY + ':' + width + ':' + height
    );
  } catch (e) {
    console.error(e);
  }
};

measureLayout();
```

awaitを使って、値を受け取ります。このあたりは非同期通信とまんまいっしょですね。


# JSへイベントの送信

私はまったくやったことないですが、ドキュメントに書いてあったので、一応ちょっと調べました。
JS側から呼び出されなくても、ネイティブモジュール側からイベント送信ができるというものです。
タイマーとか？で使えるのかと思います。

ネイティブモジュール側

```java
...
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
...
private void sendEvent(ReactContext reactContext,
                       String eventName,
                       @Nullable WritableMap params) {
  reactContext
      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
      .emit(eventName, params);
}
...
WritableMap params = Arguments.createMap();
params.putString("eventProperty", "someValue");
...
sendEvent(reactContext, "EventReminder", params);
```

ReactContextで、

```java
  reactContext
      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
      .emit(eventName, params);
```

getJSModuleというメソッドを呼び出すことで、JS側のイベントをトリガーに設定できるようです。
JS側は

```javascript
import { NativeEventEmitter, NativeModules } from 'react-native';
...

  componentDidMount() {
    ...
    const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample);
    this.eventListener = eventEmitter.addListener('EventReminder', (event) => {
       console.log(event.eventProperty) // "someValue"
    });
    ...
  }

  componentWillUnmount() {
    this.eventListener.remove(); //Removes the listener
  }
```

addListenerで上で指定したネイティブモジュールでイベントを登録できるようです。


ネイティブモジュールを作るだけでしたら、以外と簡単でした。
2年前はあんなに苦労したのに。。。