JavaScript
Android
reactjs
React
reactnative

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

More than 1 year has passed since last update.


About me

nullpoo.png


  • 名前: Koki HASHIMOTO

  • Twitter: @nullpoo

  • 所属: 株式会社メルカリ


    • フロントエンドやってます





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



なぜ


  • 開発しようとしているアプリで、音声入力を元に表示内容を切り替える機能を作りたい

  • 音声入力ダイアログを呼び出して使いたい

  • どうせならAndroidとのブリッジを書いてみようと思った


    • 数年前にAndroidアプリの開発をやっていたので多少は触れる(はず)





こういうやつ

speaknow.png



Androidのインテント


Intent は、他のアプリコンポーネントからのアクションを要求するときに使用するメッセージング オブジェクトです。

(https://developer.android.com/guide/components/intents-filters.html?hl=ja)


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



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

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

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側で付与したメソッドを呼び出すことができる

@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)

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)

@Override protected List<ReactPackage> getPackages() {

return Arrays.<ReactPackage>asList(new MainReactPackage(), new SpeechRecognizerPackage());
}



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

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

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


private Promise mSpeechRecognizerPromise;

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

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

...

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

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

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 をより直感的に扱うことができる

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 で統一したほうが引数もすっきりして良さそう