作りたいアプリがAndroidのネイティブモジュールが必要なものだったので、作ることにしました。
2年前にもちょっとさわりましたが、前回はだいぶ適当だった+忘れていることもあり、自分用のメモも兼ねて書きます。
はじめに
ReactNativeにはデフォルトである程度のネイティブモジュールが用意されています。例えば、カメラや録音など
通常その用意されたモジュールを使うので、Androidのネイティブモジュールを別段実装する必要はありません。
用意されたモジュールでは対応できない場合に実装する必要があります。
実装はAndroidJavaで行います。
Androidのネイティブモジュール
ドキュメントはここを読みました。
今回はドキュメントに書かれているToastModule(一瞬表示されるアラートのようなやつ)を参考に勘所が書けたらなと思います。
モジュールを作る(例.ToastModule.java)
①作りたいモジュールのJavaファイルを作る
作る場所はandroid/app/src/main/hjava/com/{{アプリ名}}/
{{作りたいモジュール名}}Module.java
例:ToastModule.java
というファイルを用意します。
ここで作ったモジュールが、ReactNativeから呼び出すモジュールになります。
②お作法にのっとってモジュールを作る
作った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から読みこんで使います。
②お作法にのっとって書く
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
このファイルはデフォルトで用意されていると思います。ここに
...
// 作ったパッケージ
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からモジュールを呼び出す専用のファイルを呼び出すコードを書きます。
import { NativeModules } from 'react-native';
// get_nameで指定した名前
module.exports = NativeModules.ToastExample;
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年前はあんなに苦労したのに。。。