Cover image for react-nativeでNative moduleを呼び出す(Android編)
react-nativeandroid

react-nativeでNative moduleを呼び出す(Android編)

January 31, 2019

7 min read

mitsuruogMitsuru Ogawa

react-native で Android の Native module を呼び出す方法です。基本的には下の公式ドキュメントのやり方を真似ていますが、一部そのままでは動作しなかった部分があるため、その辺りも紹介します。

紹介する内容は次の通りです。

  • 簡単な Counter を Native Module で実装した
  • Native Module の呼び出し
  • Native Module から Constants を受け取る
  • Native Module からの Callback を扱う
  • Native Module からの Promise を扱う
  • Native Module からの Event を扱う

対象のバージョンは次の通りです。

  • react-native: 0.57.8
  • Android SDK: 27(Oreo)

プロジェクト全体のコードは GitHub で見ることができます。

ちなみに Android 開発はほとんどやったことがありません。

Native Module の呼び出し

まずReactContextBaseJavaModuleを継承したCounterModule.javaを作成します。 その中にgetNameメソッドを作成して、このモジュール名を返すようにします。このモジュール名を react-native 側で利用します。

// CounterModule.java
public class CounterModule extends ReactContextBaseJavaModule {
  public CounterModule(ReactApplicationContext reactApplicationContext) {
    super(reactApplicationContext);
  }

  @Override
  public String getName() {
    return "Counter";
  }
}

続いてReactPackageを実装したCounterPackage.javaを作成します。 中身は新しく追加した react-native 側で利用できるように登録するためのものなので、あまり細かいことは気にせず、公式ドキュメントのまま実装していきます。

// CounterPackage.java
public class CounterPackage 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 CounterModule(reactContext));
    return modules;
  }
}

そしてMainApplication.javagetPackagesメソッドにCounterPackageを追加して react-native 側から参照できるようにします。

// MainApplication.java
public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {

    ...

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
+              new CounterPackage(),
              new CalendarManagerPackage()
      );
    }
  }

  ...

}

react-native 側ではNativeModulesの中に、先ほど定義したモジュール名で Native Module が渡されてくるので、これを利用します。

// App.js
import { NativeModules } from "react-native";

const { Counter } = NativeModules;

// 何かの処理
// Counter.doSomething();

これで Native Module を react-native 側で利用する準備が整いました。

Native Module から Constants を受け取る

Native Module 側から counter の初期値を返します。

CounterModule.javagetConstantsメソッドを追加します。react-native 側に渡したいものをHashMapの中に設定していきます。

// CounterModule.java

  ...

  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put("initialCount", 0);
    return constants;
  }

react-native 側では、initialCountは次のように利用することができます。

// App.js
console.log(Counter.initialCount); // => 0

Native Module からの Callback を扱う

現在の count を返すgetCountメソッドを実装します。

Callback はCallbackクラスで定義されているので、これを引数で受け取ってinvokeで Callback を実行します。

// CounterModule.java
public class CounterModule extends ReactContextBaseJavaModule {

  private int count = 0;

  ...

  @ReactMethod
  public void getCount(Callback callback) {
    callback.invoke(count);
  }
}

Native Module から react-native 側に公開するメソッドには@ReactMethodアノテーションをつける必要があります。 また、react-native と Native Module 間のやりとりは常に非同期であるため、戻り値は常にvoidになります。

react-native 側では次のように利用します。

// App.js
Counter.getCount((count) => console.log(count)); // => 0

Native Module からの Promise を扱う

次は Promise を扱ってみます。

decrementメソッドを実装します。正しく減算できた場合はresolveを、count が 0 で減算しようとした場合にrejectを返すようにします。

Promise はPromiseクラスで定義されているので、これを引数で受け取ってそれぞれresolverejectを実行します。(シンプルでわかりやすいですね)

// CounterModule.java

  ...

  @ReactMethod
  public void decrement(Promise promise) {
    if (count == 0) {
      promise.reject("E_COUNT", "count count cannot be negative.");
    } else {
      count = count - 1;
      promise.resolve(count);
    }
  }

react-native 側では通常の Promise と同じように扱うことができます。

// App.js
Counter.decrement()
  .then((count) => console.log(count))
  .catch((error) => console.error(error));

Native Module から Event を受け取る

最後に decrement した時に、onDecrementイベントが発火するようにして、これを react-native 側で利用できるようにします。

Event を react-native 側に送るにはReactContextが必要なので、this.getReactApplicationContext()から取得します。これを使ってイベントを通知します。

公式ドキュメントだとReactContextをどう取得すればいいか記載がありません。

react-native 側に送るペイロードはWritableMapを使って準備します。

// CounterModule.java

  ...

  @ReactMethod
  public void decrement(Promise promise) {
    if (count == 0) {
      promise.reject("E_COUNT", "count count cannot be negative.");
    } else {
      count = count - 1;
      promise.resolve(count);

      // react-native側へ送るペイロード
      WritableMap params = Arguments.createMap();
      params.putInt("count", count);

      // react-native側にイベントを発火する
      this.getReactApplicationContext()
        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
        .emit("onDecrement", params);
    }
  }

react-native 側ではNativeEventEmitterの中に Native Module のインスタンスを設定して EventEmitter を取得します。 あとは、EventEmitter に EventListener を設定すれば OK です。

// App.js
import { NativeModules, NativeEventEmitter } from "react-native";

const counterEventEmitter = new NativeEventEmitter(Counter);

counterEventEmitter.addListener("onDecrement", ({ count }) => {
  console.log(count); // => 1
});

なにやらDeviceEventEmitterNativeAppEventEmitterというものもあるらしい。

まとめ

react-native で Android の Native module を呼び出す方法についてでした。 2 つのものが繋がって動作すると楽しいですね。