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

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

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つのものが繋がって動作すると楽しいですね。