Edited at
CureAppDay 9

react-nativeでAndroidのAPIを扱う(Native Modules編)

More than 1 year has passed since last update.

この記事はCureApp Advent Calendar 2016の9日目の記事です。


この記事の目的

まだReactNativeがサポートしていないAPIを使いたい場合、既存のJavaコードを再利用したい場合、あるいはマルチスレッドなどを用いたい場面では、Android-Java APIを直接叩ける必要があります。

そこに対して、React Nativeでサポートしているのですが、公式の説明やコード例が十分でない&日本語の資料もなさそうなので、まとめてみます。

※公式が不十分でソースコードを読みつつの理解となるため、正しくない内容になってしまうかもしれない点をご了承ください。(そもそも正解がない)

※iOS版はこちらをどうぞ→react-nativeでObjective-C/SwiftのAPIを扱う(Native Modules編)


必要なこと

最終的にはJSコードからAndroid-Java APIを叩けるようにしたいですよね。

そのためにReact Nativeでは、


  1. ReactContextBaseJavaModuleを継承したクラスを作り、React Native用のアノテーションを付けたメソッドでAndroid-Java APIの操作を書く

  2. ReactPackageを継承したクラスを作り、そこで上記1で作ったクラスを読ませるようにする。

  3. 上記2で作ったクラスを MainApplication.java で読み込む

  4. JSコードで、nativeコードを呼ぶ宣言を書いておく。

以上を行うことで、実行時にマクロで良い感じに解決してくれます。(詳細はまだ調べてないです。。。)

ここでは、公式にもあるAndroidのToastを例に取って、Android-Java APIを呼ぶところまでを順番に見ていきましょう。

ちなみに、ソースコードはこちらに置いています。

https://github.com/uryyyyyyy/RNBindingSample/tree/android-native-module


ReactContextBaseJavaModuleを継承したクラスを作る

ここで作るAPIは、JS側から呼ぶときは MyToastAndroid としたいので、クラス名はMyToastModule.java などとしておきます。

コードはこのような形です。(今回呼びたい処理は Toast.makeText() の箇所です。)


MyToastModule.java

import android.view.Gravity;

import android.widget.Toast;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.MapBuilder;

import java.util.Map;

//①
public class MyToastModule extends ReactContextBaseJavaModule {

public MyToastModule(ReactApplicationContext reactContext) {
super(reactContext);
}

//②
@Override
public String getName() {
return "MyToastAndroid";
}

//③
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = MapBuilder.newHashMap();
constants.put("SHORT", Toast.LENGTH_SHORT);
constants.put("LONG", Toast.LENGTH_LONG);
return constants;
}

//④
@ReactMethod
public void show(final String message, final int duration) {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
});
}
}


コードの説明をします。

①でReactContextBaseJavaModuleを実装していますね。

②では、 ReactContextBaseJavaModule を実装するにあたって getName() をoverrideする必要があるため記述しています。

ここで付けた MyToastAndroid は、後述するJSコードの中でNativeモジュールを呼ぶ時の識別子となります。

③では、 getConstants() を実装していますが、こちらの記述は任意です。今回は表示時間の制御フラグに使えるため記述しています。

④では、定義している show というメソッドをJSコードから参照できるようにするため、 @ReactMethod というアノテーションを付与しています。

ちなみに、durationはgetConstants()から取れそうだというのは問題ないとして、JavaのStringなどはJSではどの型で表現すればいいのでしょうか?対応表はこちらです。

Java -> JS

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


ReactPackageを継承したクラスを作る

次に、作ったクラスをアプリケーションに登録する必要があります。その時にはパッケージングを刷る形になります。

今回は1ファイルだけなので冗長に感じるかもしれませんが、複数ファイルをまとめて一機能としたいケースに有用そうです。


MyToastPackage.java

package com.rnbindingsample.mytoast;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
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 MyToastPackage implements ReactPackage {

//①
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}

//②
@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 MyToastModule(reactContext));
return modules;
}
}


①、②はそれぞれ「JavaからJSコードを呼ぶ場合」「JavaのNative Componentを扱う場合」に使う箇所のようでして、今回は扱いません。

③のところで、先ほどのMyToastModuleを設定していることがわかりますね。


MainApplicationに組み込み


MainApplication.java

    @Override

protected List<ReactPackage> getPackages() {
return Arrays.asList(
new MainReactPackage(),
new MyToastPackage()
);
}

作ったパッケージ(MyToastPackage)をココで読み込ませることで、このアプリ内でパッケージが使えるようになります。

ここまででJava側の記述は完了です。最後にJSコードを見ていきましょう。


JS側でラッパーを用意する

上記の記述で普通にJSで動かせるようになります。ただ、NativeModulesを呼ぶコードがあちこちに散乱するのも嫌なので、大抵はそれをラップしたクラスを噛ませて、アプリではそこからしかNative APIを呼ばないようにしています。(こうすることで、ライブラリ化やAndroid/ios出し分けがしやすくなります。何より責務がちゃんと分かれます。)


MyToastAndroid.js

import {NativeModules} from 'react-native';

export default NativeModules.MyToastAndroid;

これで全ての準備が完了です。


Native APIをJSから呼んでみる


index.android.js

...

import MyToastAndroid from './MyToastAndroid';

...

componentDidMount(){
MyToastAndroid.show('Awsome', MyToastAndroid.SHORT)
}


このように使えば、アプリ起動時にtoastで Awsome が表示されるようになります。


まとめ

上記のようにして、比較的簡単(かつ大きくなっても破綻しないように考えられている)にNative APIを触れるようになりました。

ドキュメントではcallbackの例があったのでリポジトリの方では試してみています。よければ合わせてご覧ください。

再掲:https://github.com/uryyyyyyy/RNBindingSample/tree/android-native-module

他にも、PromiseやThreading、Events発火、アプリのライフサイクル管理なども組み込むことが出来るそうです。折をみてこちらの資料も更新していければと思っています。


おまけ


  • Javaコードを頻繁に更新する場合、Gradle Daemonを立てておくとビルドが早くなります。

  • Androidエミュレータでのデバッグは「Ctrl + M」です。