React Nativeハンズオン〜Native Modules を作る(Android版)〜

Native の API を利用したいが、ReactNative がサポートしていない場合自前でモジュールを作る必要があります。
やり方は[公式のドキュメント](https://facebook.github.io/react-native/docs/native-modules-android.html)で紹介されているので、流れに沿ってハンズオンを行います。
Macの方をメインターゲットにしているので、Windowsの方は適宜読み替えてください。

成果物はここにあります。
https://github.com/OshiroSeiya/sample_android_native_modules

# 環境構築

[公式のドキュメント](https://facebook.github.io/react-native/docs/getting-started.html)を参考に入れていきます。

**注意** :Androidの開発環境構築はミスし易いので手順をしっかり確認してください
yarn は僕が使いたいので入れます。

```
$ brew install node
$ brew install yarn
$ brew install watchman
$ npm install -g react-native-cli
```

## Java Development Kitの導入

[ダウンロードページ](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)

## AndroidStudio の導入

[ダウンロードページ](https://developer.android.com/studio/index.html)

インストールタイプを選択するように求められたら、「Custom」セットアップを選択します。
次のすべてのチェックボックスがオンになっていることを確認します。

* Android SDK
* Android SDK Platform
* Performance (Intel ® HAXM)
* Android Virtual Device

「次へ」をクリックしてこれらのコンポーネントをすべてインストールします。
セットアップが完了し、ウェルカム画面が表示されたら、次の手順に進みます。

## Android SDK の導入

Android Studioはデフォルトで最新のAndroid SDKをインストールします。
リアクションの構築ネイティブコードを使ったネイティブアプリは、特にAndroid 6.0（Marshmallow）SDKが必要です。
追加のAndroid SDKは、Android StudioのSDKマネージャからインストールできます。
SDKマネージャには、「Welcome to Android Studio」画面からアクセスできます。
「Configure」をクリックし、「SDK Manager」を選択します。
「Show Package Details」にチェックを入れて詳細を見れるようにします。
次のすべてのチェックボックスがオンになっていることを確認します。

Android 6.0 (Marshmallow)

* Google APIs
* Android SDK Platform 23
* Intel x86 Atom_64 System Image
* Google APIs Intel x86 Atom_64 System Image

次に「SDK Tools」タブを選択して「Android SDK Build-Tools」の `23.0.1` を選択します。

問題なければ「Apply」を選択してインストールします。

## 環境変数の設定

`.bash_profile` などに下記を追加します。

```
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools
```

編集が終わったら設定をロードします。

```
source $HOME/.bash_profile
```

## プロジェクトの作成

```
$ react-native init sample_android_native_modules
```

## Androidエミュレータの準備

React Native の Androidアプリを実行するには、Androidデバイスが必要です。
これは物理的なAndroidデバイスでも、エミュレータでもいいですが、やりやすいようにエミュレータを利用することにします。

まず、Android Studio でプロジェクトを開きます。
プロジェクトは `./sample_android_native_modules/android` を指定してください。
起動後に Gradle をアップデートするか確認されるかもしれないですが、今回はアップデートなしを選択してください。

その後 「Tools」をクリックし「Android」を選択「AVD Manager」を選択します。

「Create Virtual Device...」をクリックして新しい仮想デバイスを作成します。
「phone」から好きなデバイスを選んで「Next」を選択します。
「x86 Images」タブを選択して「Marshmallow 23 x86_64 Android 6.0 (Google APIs)」を選択します。
「Next」を選択後「Finish」を選択します。

緑色の再生マークをクリックしてエミュレータを起動します。

React Native プロジェクトのディレクトリに移動してアプリを実行します。

```
$ cd sample_android_native_modules
$ react-native run-android
```

エミュレータに下記のような画面が表示されれば成功です。

![参考画像](https://facebook.github.io/react-native/docs/assets/GettingStartedAndroidSuccessMacOS.png)

# Native Modules チュートリアル開始

Toast を実装して行きます。JavaScriptからトーストメッセージを作成できるようにします。

## モジュールの作成する

まず、Native Modules は `ReactContextBaseJavaModule` を拡張することで JavaScript から呼び出せる機能を実装できるので `ToastModule` クラスを実装します。

```java:android/app/src/main/java/com/sample_android_native_modules/toast/ToastModule.java
package com.sample_android_native_modules.toast;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

public class ToastModule extends ReactContextBaseJavaModule {
    public ToastModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }
}
```

`ReactContextBaseJavaModule` は `getName` というメソッドが実装されている必要があります(overrideしましょう)。
`getName` で定義した文字列をもとに JavaScript からNative Modules を呼び出すことができます。
チュートリアルでは `ToastExample` という名前で実装します。

```diff:android/app/src/main/java/com/sample_android_native_modules/toast/ToastModule.java
package com.sample_android_native_modules.toast;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

public class ToastModule extends ReactContextBaseJavaModule {
    public ToastModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

+    @Override
+    public String getName() {
+        return "ToastExample";
+    }
}
```

`getConstants` はオプションです。
機能は、JavaScriptに公開されている定数値を返します。
 必須ではないですが、JavaScriptからJavaに同期して通信する必要のある事前定義された値をキーするには便利です。
チュートリアルでは表示時間の制御に使うため記載しています。

```diff:android/app/src/main/java/com/sample_android_native_modules/toast/ToastModule.java
package com.sample_android_native_modules.toast;

+ import android.widget.Toast;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

+ import java.util.Map;
+ import java.util.HashMap;

public class ToastModule extends ReactContextBaseJavaModule {
+    private static final String DURATION_SHORT_KEY = "SHORT";
+    private static final String DURATION_LONG_KEY = "LONG";

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

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

+    @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;
+    }
}
```

実装したメソッドを JavaScript に公開するには、 Java メソッドに `@ReactMethod` を使用して注釈を付ける必要があります。 戻り型は常に無効です。 
React Native のブリッジは非同期であるため、結果をJavaScriptに渡すためには、コールバックまたはイベントを発行します。(今回はやりませんがドキュメントには記載されているので興味わいた方は試してみてください)

```diff:android/app/src/main/java/com/sample_android_native_modules/toast/ToastModule.java
package com.sample_android_native_modules.toast;

import android.widget.Toast;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
+ import com.facebook.react.bridge.ReactMethod;

import java.util.Map;
import java.util.HashMap;

public class ToastModule extends ReactContextBaseJavaModule {
    private static final String DURATION_SHORT_KEY = "SHORT";
    private static final String DURATION_LONG_KEY = "LONG";

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

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

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

+    @ReactMethod
+    public void show(String message, int duration) {
+        Toast.makeText(getReactApplicationContext(), message, duration).show();
+    }
}
```

引数の型は下記のように対応します。

| Java | JavaScript |
| --- | --- |
| Boolean | Bool |
| Integer | Number |
| Double | Number |
| Float | Number |
| String | String |
| Callback | function |
| ReadableMap | Object |
| ReadableArray | Array |

ReadableMap と ReadableArray は React Native 独自のものなので github を参照してください
[ReadableMap](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java)
[ReadableArray](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java)

## モジュールの登録する

JavaScript から呼び出すためにはモジュールをアプリケーションに登録する必要があります。
`ReactPackage` を継承したクラスを作成し、パッケージリストに追加します。 

```java:android/app/src/main/java/com/sample_android_native_modules/toast/ToastPackage.java
package com.sample_android_native_modules.toast;

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

パッケージは、`MainApplication.java` ファイルの `getPackages` メソッドで提供する必要があります。

```diff:android/app/src/main/java/com/sample_android_native_modules/MainApplication.java
package com.sample_android_native_modules;

import android.app.Application;

import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
+ import com.sample_android_native_modules.toast.ToastPackage;

import java.util.Arrays;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

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

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}
```

## JavaScript からの呼び出しし易いようにラッパー用意する

JavaScriptから新しい機能に簡単にアクセスできるようにするには、ネイティブモジュールをJavaScriptモジュールでラップするのが一般的です。

```javascript:lib/Toast.js
import { NativeModules } from 'react-native';
export default NativeModules.ToastExample;
```

## ボタン配置してクリックされたタイミングで表示する

```diff:App.js
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
+  Button,
  View
} from 'react-native';
+ import Toast from './lib/Toast';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' +
    'Cmd+D or shake for dev menu',
  android: 'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit App.js
        </Text>
        <Text style={styles.instructions}>
          {instructions}
        </Text>
+        <Button
+          onPress={() => Toast.show('表示できた！', Toast.SHORT)}
+          title="Show Toast"
+          accessibilityLabel="Show toast"
+        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});
```

ボタンを押してこんな感じになったら完成です！

<img width="274" alt="02gazou.png" src="https://qiita-image-store.s3.amazonaws.com/0/82745/1388c7d0-cfb2-af01-272a-5bc236f6865c.png">

# 時間のある人は

実は Toasta はすでに実装されています。
[ドキュメント](https://facebook.github.io/react-native/docs/toastandroid.html)
[ソースコード](https://github.com/facebook/react-native/blob/v0.54.0/ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java)

下記のメソッドを作成してみましょう！

* showWithGravity
* showWithGravityAndOffset

# まとめ

実際に書いてみて思ったよりかんたんに Native Module が作れたと思います。