LoginSignup
1
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-31

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

はじめに

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

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

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

実装はAndroidJavaで行います。

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;
  }

}

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

modules.add(new ToastModule(reactContext));

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

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で返せます。
ブリッジ通信は非同期であり、呼び出しと値の受け取りはちょっと考える必要があります。

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

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側では、

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

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

また、Promiseの場合は、

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側では、

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側から呼び出されなくても、ネイティブモジュール側からイベント送信ができるというものです。
タイマーとか?で使えるのかと思います。

ネイティブモジュール側

...
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で、

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

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

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年前はあんなに苦労したのに。。。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2