スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Android対応から見つめるReact Native

モバイルエンジニアの@chiiia12です。

先日@m-sugawaraからReact Native開発全般についての記事が公開されましたが、今回はAndroid対応にフォーカスして紹介します。

QuipperではReact Nativeで書かれた業務用アプリがあり、iOSでのみ提供していました。対象ユーザーは内部のスタッフに限られていたため、会社から配布している業務用iOS端末のみで正しく動作すれば十分だったためです。しかし私用端末でも使える方が業務上効率が良いこと・業務用の端末を用意するコストの観点から、会社からの端末配布をやめ、iOS/Android両プラットフォームでアプリを提供し私用端末で利用してもらうことになりました。

今回は私達のチームが遭遇したReact NativeアプリでのAndroid対応をサプライズ度(★★★)と一緒に紹介します。React Nativeのマルチプラットフォーム対応にはどんなものがあるのか、参考のひとつになればうれしいです。 なお、執筆時点でReact Nativeのversionは0.49.0を使用しています。

ListItemの背景色 ★☆☆

NativeBaseのListItemを使っている際にAndroidのみ背景色が透明になってしまうことがありました。 こちらはReactをver 16.6以上に設定をupdateしたことに起因していたようでした。cssでbackgroundColorを設定して解決します。

Before After
screenshot_20190226-124123 screenshot_20190226-124921

ソフトウェアキーボードの改行の挙動 ★☆☆

iOSでは問題なく複数行のテキスト入力ができましたが、AndroidではEnterボタンがテキスト入力の終了となってしまい改行が入力できないということがありました。

android_softkeyboard

複数行入力させたい場合Androidでは以下の対応が必要でした。

<TextInput
textInputProps={
            Platform.OS === 'android'
                ? {
                    returnKeyType: 'none',
                    blurOnSubmit: false,
                  }
                : {}
}
/>

React NativeのTextInputのdocumentによると TextInput に入力が終わった時の挙動を設定できるreturnKeyTypenone を設定することができます(Androidのみ)。またこの設定だけでは英語キーボードのみでしか改行できないようでした。日本語キーボードでも改行を適用させるためには blurOnSubmitの設定が必要のようです。

ref: https://github.com/facebook/react-native/issues/12717#issuecomment-301014327

If true, the text field will blur when submitted. The default value is true for single-line fields and false for multiline fields. Note that for multiline fields, setting blurOnSubmit to true means that pressing return will blur the field and trigger the onSubmitEditing event instead of inserting a newline into the field.

blurOnSubmitのdocumentによると、以上のようにblurOnSubmitがtrueの場合はmultiline入力に対応しておらずonSubmitEditingが発火されてしまうことが関係しているようでした。

MultiDex対応 ★★☆

method数が64Kを超えたのでMultiDex対応をしました。基本的にはbuild.gradleを追加するだけでMultiDexは完了しますが、react-native-navigationを使っている時のみ、MainApplication.javaに以下の記述が追記が必要になります。

ref: https://github.com/wix/react-native-navigation/issues/1925#issuecomment-332012108

 @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

余談ですが、CIでのBuildが失敗しmethod数が64Kを超えたのに気づいたのはリリース直前で、文言変更だけの小さなPull Requestをマージしたタイミングでした。その前にマージされた機能によってローカルでも64K問題は起きていましたが、なぜかCI環境ではしばらくBuildが成功しており気づくのが遅れました。そんなリリース前の心折れる出来事がサプライズ度をあげています。

Staging用BuildTypeの追加 ★★☆

リリース前のアプリを内部確認用としてDeployGate経由で配信するためにStaging用のBuildTypeを追加する作業が必要でした。React NativeではデフォルトでDebug/ReleaseのBuildTypeを作成しますが、DebugのBuildTypeで作ったAPKをそのままDeployGateにデプロイすると、bundleされたJSが組み込まれていないために起動できなくなってしまいます。

BuildTypeの追加後、app/build.gradleに以下のようにStagingBuildでのbundleの設定が追加できます。

project.ext.react = [
        entryFile : "index.js",
        bundleInStaging: true,
]

しかし、buildは思ったとおりに動かず以下のようなエラーが発生しました。

error: uncompiled PNG file passed as argument. Must be compiled first into .flat file..

私達のアプリではAAPT2をfalseにすること、/node_modules/react-native/react.gradleをカスタマイズした上でBuildするようにして回避しています。

ref: https://github.com/react-navigation/react-navigation/issues/3097#issuecomment-354511336 https://stackoverflow.com/questions/47084810/react-native-android-duplicate-file-error-when-generating-apk

doLast {
    def moveFunc = { resSuffix ->
        File originalDir = file("${resourcesDir}/drawable-${resSuffix}")
        if (originalDir.exists()) {
            File destDir = file("${resourcesDir}/drawable-${resSuffix}-v4")
            ant.move(file: originalDir, tofile: destDir)
        }
    }
    moveFunc.curry("ldpi").call()
    moveFunc.curry("mdpi").call()
    moveFunc.curry("hdpi").call()
    moveFunc.curry("xhdpi").call()
    moveFunc.curry("xxhdpi").call()
    moveFunc.curry("xxxhdpi").call()
}

// Set up inputs and outputs so gradle can cache the result

以上のコードをreact.gradledoFirstブロックの後ろに書き足すようにbuild時のjobに組み込んでいます。

CodePushの設定見直し ★★★

CodePushを使ったOTA配信を利用しているため、Androidにも対応させます。CodePushのversionがアプリ上で表示されていない問題があり、調査してみると実際にはCodePushから正しくbundleされたJSが配信されていないことがわかり設定を見直していきます。

code-pushコマンドを使って実際にinstallされた数を確認できます。

code-push deployment history <appName> <deploymentName>

結果を見ると一度もinstallされていないことがわかります。

ss 2019-04-01 at 19 35 34

調査の結果、CodePush上のversionとbuild.gradle上で指定されたversionがあっていないと正しく配信されないことがわかり確認します。

ss 2019-04-01 at 19 26 14

AppCenterのCodePushのTargetVersionsの部分です

 defaultConfig {
        ...
        versionName "1.5.1"
    }

CodePushが提供しているdeploy用のscriptではTARGET_BINARY_VERSIONというファイルを用いてCodePushを配信する対象のアプリversionを指定しています。build.gradle内で定義したversionがCodepushの配信対象に含まれているか確認する必要があります。

ref: https://docs.microsoft.com/en-us/appcenter/distribution/codepush/cli#target-binary-version-parameter

ss 2019-04-01 at 19 35 46

変更後code-pushコマンドで確認するとCode Push経由で配信されていることがわかります。変更点はすごく単純なものですが、原因を突き止める調査には時間がかかる長い旅でした。

[番外編]Android 9で起きていた未解決な事案

以下はまだ解決には至ってないものですが、Android 9の端末では以下のようなことが発生しました。

コンポーネントの下部が切れる

メッセージを表示しているコンポーネントの下部が切れるというものがあります。おそらく日本語が含まれた時のコンポーネントの高さの計算がうまくいっていないように見受けられます。

ss 2019-04-02 at 19 34 35

React Nativeのversion0.57.4で直っているようです。 弊チームでは色々色々あって*1まだReact Nativeのバージョンアップができていないのです。。。

inverted scrollが効かない

下から上に読み込みながらのスクロールを実現するreact-native-invertible-scroll-viewを使っている際にAndroid 9の端末では慣性スクロールがうまく効かない挙動になることがありました。

scroll_gif

勢いよくスクロールしようとすると意図したスクロールの方向とは逆の方向にスクロールします。 React Nativeのissue内でも議論がされている内容のようです。 workaroundも紹介されていますが我々のアプリはreact-native-gifted-chatを使用しており、その内部でreact-native-invertible-scroll-viewを使用と独自のScrollViewを実装しそのクラスを使うには少々複雑な状況になっており、調査と検証が頓挫しています。

おわりに

突然React Nativeのタスクを担当することになって約3ヶ月。最初はTypeScriptやReact/Reduxの考え方に新しく触れ、修正したものがすぐアプリに反映される感覚に興奮していた気がします。 Androidのみで起こる問題に携わっていくにつれ、だんだんビルド周りの設定の調査や改修する時間が長くなっていったように感じます。ビルド周りの問題は自分の知識の浅さも相まってかなり手探りな状態でTry&Errorするしかなく、ネット上での情報の少なさもあり時間がかかってしまいました。また、React Nativeアプリで起こる不具合は、通常のNative開発で感じるような「あっここがこうなってバグっているんだな」となんとなく推測できる不具合は少なく、調査する箇所を絞り込むことができない苦しさを感じることも多かったです。普段Android開発をしている自分にとって、JavascriptiOSアプリをかけることはポジティブに思っていますが、マルチプラットフォームできちんと動くアプリを書くということの大変さを感じた3ヶ月間でした。

React NativeのAndroidのみ対応すべき事項は他にもたくさんあるとは思いますが、私達のアプリで起きたことを紹介しました。React Nativeは手探りなことが多く日々いろんな事が起きるのですが、こういった出来事は今後の開発に活かされていく予定です。私達の挑戦は続きます。

*1:ローカルでは動くがCI環境でのBuildに失敗する問題がありまして…