async / await 構文を使った Android とのブリッジ

## About me

![nullpoo.png](https://qiita-image-store.s3.amazonaws.com/0/28643/9f32e45c-7d2b-a36c-6c8a-1a001f2c78c9.png)


- 名前: Koki HASHIMOTO
- Twitter: @nullpoo 
- 所属: 株式会社メルカリ
    - フロントエンドやってます

---

## Android のインテントを使って、処理結果を受け取りたい

---

### なぜ

- 開発しようとしているアプリで、音声入力を元に表示内容を切り替える機能を作りたい
- 音声入力ダイアログを呼び出して使いたい
- どうせならAndroidとのブリッジを書いてみようと思った
    - 数年前にAndroidアプリの開発をやっていたので多少は触れる(はず)

---

### こういうやつ

![speaknow.png](https://qiita-image-store.s3.amazonaws.com/0/28643/a03f54c2-d57b-e379-bc64-c9b17b394122.png)

---

### Androidのインテント

> Intent は、他のアプリコンポーネントからのアクションを要求するときに使用するメッセージング オブジェクトです。
(https://developer.android.com/guide/components/intents-filters.html?hl=ja)

カメラの起動、URLをブラウザで開くなどのAndroidの各機能や、他アプリの起動など連携のための仕組み

---

### Androidでインテント遷移先の結果をハンドリング

startActivityForResultで対象Activityを呼び出し、遷移元のActivityではonActivityResultをOverrideして結果を受け取る

```java
public static final int REQUEST_CODE_EXMAPLE = 1000;

  private View.OnClickListener mClickListener = new View.OnClickListener() {
    @Override public void onClick(View v) {
      // SecondActivityの呼び出し
      Intent intent = new Intent(getApplicationContext(), SecondActivity.class);
      startActivityForResult(intent, MainActivity.REQUEST_CODE_EXMAPLE);
    }
  }

  @Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == MainActivity.REQUEST_CODE_EXMAPLE) {
      Log.d("EXAMPLE", data.getStringExtra("EXAMPLE_STRING"));
    }
  }
```

呼び出し元メソッドと別のメソッドで結果を受け取るため、複数遷移先がある場合など規模が大きくなると処理が散らかりがち
  - RxJavaでストリームとして処理するような仕組みを作ればスマートにかけるかもしれない

---

## React Native でブリッジするには

---

### Android側でJS側に公開するメソッドを作成

@ReactMethod を付与するとReact native側で付与したメソッドを呼び出すことができる

```java
@ReactMethod public void startRecognition() {
  Activity currentActivity = getCurrentActivity();

  // Create Intent
  Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
  intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
      RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
  intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
      getReactApplicationContext().getPackageName());

  // 遷移するだけで結果は受け取らない
  currentActivity.startActivity(intent);
}
```

---

### React Native側に公開するパッケージを作成&登録

パッケージを作成(MainActivity.java)

```java
public class SpeechRecognizerPackage implements ReactPackage {
  @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    List<NativeModule> nativeModules = new ArrayList<>();
    nativeModules.add(new SpeechRecognizerModule(reactContext));
    return nativeModules;
  }
  ...
```

パッケージを登録(MainApplication.java)

```java
@Override protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(new MainReactPackage(), new SpeechRecognizerPackage());
}
```

---

### JS側で公開されているメソッドを呼び出す

NativeModulesに登録したパッケージが生えているので呼び出す

```js
import { NativeModules } from 'react-native';

const SpeechRecognizerAndroid = NativeModules.SpeechRecognizerAndroid;

recognition() {
  try {
    SpeechRecognizerAndroid.startRecognition();
  } catch (e) {
    console.log(e);
  }
}
```

---

## async / await を使ってstartActivityForResultの結果を受け取る

---

### ReactMethodでPromiseを扱うことができる

**ReactMethodの最後の引数にPromiseを追加**すると、React native側でPromiseオブジェクトを注入してくれる
> https://facebook.github.io/react-native/docs/native-modules-android.html#promises

```java
private Promise mSpeechRecognizerPromise;

@ReactMethod public void startRecognition(Promise promise) {
  ...

  // onActivityResultで値を渡せるように保持
  mSpeechRecognizerPromise = promise;

  ...

  // 遷移先の結果を受け取れるように呼び出す
  currentActivity.startActivityForResult(intent, Constants.SPEECH_RECOGNIZER_REQUEST_CODE);
}
```

Promise#resolve で　正常系、rejectで異常系として返す

```java
public SpeechRecognizerModule(ReactApplicationContext reactContext) {
  super(reactContext);
  reactContext.addActivityEventListener(new ActivityEventListener() {
    @Override
    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
    if (requestCode == Constants.SPEECH_RECOGNIZER_REQUEST_CODE
      && resultCode == Activity.RESULT_OK) {
        // 処理結果を取得して、Promiseに値を渡す
        List<String> candidates = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
        mSpeechRecognizerPromise.resolve(candidates.get(0));
      }
    } else {
      mSpeechRecognizerPromise.reject("ERROR XXX", "Error");
    }
    mSpeechRecognizerPromise = null;
    ...
  });
}
```

---

### async / await でブリッジ処理を扱う

async / await を使えば Promise をより直感的に扱うことができる

```jsx
async recognition() {
  try {
    // 遷移先の結果が返ってくるのを待って表示に反映する
    const candidate = await SpeechRecognizerAndroid.startRecognition();
    this.setState({candidate});
  } catch (e) {
    console.log(e);
  }
}

render() {
  return (
    <div>{this.state.candidate}</div>
  );
}
```

---

## まとめ

- Callbackを渡す方法やAndroid→JSのブリッジを作って結果を渡す方法もある
- しかし Promise を async / await でハンドリングするほうがスマートに書ける
- 同期処理でない処理を行うブリッジは Promise で統一したほうが引数もすっきりして良さそう