React Native製アプリのクオリティを上げるために工夫した事

React Native製アプリのクオリティを上げるために工夫した事
InkdropというMarkdownノートアプリを一人で作っているTAKUYAです。最近、React Nativeを使って、iOS版とAndroid版の新しいバージョンをリリースしました。React Nativeは、JavaScriptとReactを使ってクロスプラットフォームなモバイルアプリが開発できるフレームワークです。
どうすればReact Nativeでハイクオリティなアプリが作れるのか、今回の開発を通して多くのことを学びました。本稿では、よりよいアプリを作るために自分が工夫したことをシェアします。既にReact Nativeでアプリを作っている方も、これから作ろうと思っている方も参考になるかと思います。
- OSSライブラリは慎重に選ぶ
- ネイティブ拡張モジュールは出来るだけ使わない
- UIテーマの対応
- タブレットの対応
- 動作を軽く保つ
- 違和感のないスプラッシュスクリーンを作る
- CodePushは使わない方が良い
iOSのUIKitと違って、React Native自体はクオリティの高いUIや画面遷移を作れるモジュールを提供しません。なぜならこのフレームワークは、ReactとJavaScriptを使ってUIのレンダリングやデバイスAPIへのアクセスを提供することに注力しているからです。なので、イメージ通りのクールなUIを作ろうと思うと沢山の労力を伴います。
しかしながら、そんな問題を解決してくれる様々なライブラリがOSSで公開されています。これらを上手く活用することで、手間を掛けずにプロフェッショナルなものが作れます。以降より、Inkdropで使用したライブラリをご紹介します。

上記画像をご覧の通り、アプリにはサイドバーがあったり、モーダル画面やスタック状の画面遷移が見られます。
これらのルーティングやナビゲーションを実現するために、react-navigationを使いました。これを使えば簡単に柔軟なルーティングや滑らかな画面遷移を導入できます。注意点は、モーダル画面に対応していないことです。そこで react-native-modal を併用します。これでアニメーション付きでカスタマイズ可能なモーダル画面を導入できます。
これらのライブラリは特にこだわりが無い限りかなりおすすめです。もしすごく凝ったことがしたいなら、こちらの記事が参考になるでしょう。
iOSとAndroidではUIのガイドラインが異なります。それぞれのプラットフォームのスタイルに適合したボタンやテーブル、ヘッダなどを自前で作っていてはキリがありません。世の中には見た目の良いコンポーネントが沢山公開されていますが、これらをほいほいアプリに導入するのも考えものです。アプリが徐々に肥大化して不安定になるからです。

そこでオススメしたいのが、NativeBaseです。これはReact Native向けに作られた、クロスプラットフォームに対応したUIコンポーネント群を提供するライブラリです。React Native版 Bootstrapと言えば分かりやすいでしょう。NativeBaseはデザインの良いコンポーネント群だけでなく、レイアウト系のコンポーネントも含んでいるのが便利なポイントです。コンポーネントは自動でプラットフォームに合わせて切り替わるので、心置きなくアプリ作りに集中できます。
React Nativeはまだまだ未成熟の技術なので、APIが頻繁に変わります。そしてバージョンを上げるたびにどこかが壊れます。もし問題がネイティブ側に存在したら、それを解決するのは至難の業でしょう。それはライブラリの作者にとっても同じです。なぜなら:
We found that most React Native Open source projects were written by people who had experience with only one or two. — AirbnbReact Nativeのオープンソースプロジェクトは、1つか2つしかプラットフォームの開発経験を持たない人が作ったものだ — Airbnb
つまり、彼らは必ずしも全てのプラットフォームに精通している訳ではないのです。僕もReact Native用のSQLite3のプラグインを作って公開していますが、iOSとAndroidの両方をメンテするのは大変です。コントリビュータがWindows対応も増やしてくれましたが、僕はそちらのことは全くわからないという状態です。
もしネイティブ拡張をするライブラリをインストールしようと考えているのなら、これらのことを念頭に置いてください。僕は結局、デバッグのために頻繁にネイティブコードを読む必要がありました。この苦しみは、ネイティブ拡張を出来るだけ避けることで軽減できるでしょう。
以下は、Inkdropが使用しているネイティブ拡張ライブラリの一覧です:
- react-native-wkwebview-reborn
- react-native-image-picker
- react-native-japanese-tokenizer
- react-native-sqlite-2
より少ないネイティブ拡張依存は、あなたのアプリをよりメンテしやすくして、React Nativeの将来のバージョンに適応しやすくします。

React NativeでUIのテーマに対応するのは中々チャレンジングです。なぜなら、それはViewのレンダリング方法に大きく左右されるからです。iOSではAppearance Proxy (UIAppearance)が提供されていて簡単に標準コンポーネントの見た目を変えられますが、React NativeにはそのようなAPIは提供されていません。自前で用意する必要があります。
幸い、NativeBaseはテーミングに対応しています。以下のように変数を定義することで、NativeBaseのコンポーネントの見た目をカスタマイズ出来ます:
<span id="5d7a" class="pi pj io rv b gz rz sa m sb sc">const $defaultBgColor = '#2E3235'<br></br>const $defaultFgColor = 'rgba(255, 255, 255, 0.7)'</span><span id="2295" class="pi pj io rv b gz sd sa m sb sc">const nativeBaseTheme = { <br></br> toolbarBtnColor: $defaultFgColor,<br></br> toolbarBtnTextColor: $defaultFgColor,<br></br> toolbarDefaultBg: $defaultBgColor,<br></br> toolbarDefaultBorder: 'rgba(0, 0, 0, 0.3)',<br></br>}</span><span id="3706" class="pi pj io rv b gz sd sa m sb sc"><StyleProvider variables={nativeBaseTheme}><br></br> <View>...</View><br></br></StyleProvider></span>
しかしこれだけでは不十分で、NativeBase以外のコンポーネントの見た目は依然切り替え出来ません。そこで、react-native-extended-stylesheetを採用しました。これは、次のようにStyleSheetで変数が使えるようにしてくれるライブラリです:
<span id="9dbb" class="pi pj io rv b gz rz sa m sb sc">// app entry: set global variables and calc styles<br></br>EStyleSheet.build({<br></br> $bgColor: '#0275d8'<br></br>});</span><span id="1573" class="pi pj io rv b gz sd sa m sb sc">// component: use global variables<br></br>const styles = EStyleSheet.create({<br></br> container: {<br></br> backgroundColor: '$bgColor'<br></br> }<br></br>});</span><span id="bf72" class="pi pj io rv b gz sd sa m sb sc"><View style={styles.container}><br></br>...<br></br></View></span>
簡単ですね。これで見た目を動的に切り替え出来るようになりました!
注意: NativeBaseはStyleProviderがスタイルをキャッシュしているので、テーマを適用するにはアプリを再起動する必要があります。

例えばタブレット用に2カラムのレイアウトを表示したいときは以下のようにします:
<span id="83dd" class="pi pj io rv b gz rz sa m sb sc">const styles = StyleSheet.create({<br></br> container: {<br></br> flex: 1,<br></br> flexDirection: 'row'<br></br> },<br></br> leftViewContainer: {<br></br> flexShrink: 0,<br></br> flexGrow: 0,<br></br> width: 200<br></br> },<br></br> rightViewContainer: {<br></br> flex: 1<br></br> }<br></br>})</span><span id="f5f9" class="pi pj io rv b gz sd sa m sb sc"><View style={styles.container}><br></br> <View style={styles.leftViewContainer}><br></br> ...<br></br> </View><br></br> <View style={styles.rightViewContainer}><br></br> ...<br></br> </View><br></br></View></span>
しかしながら、画面サイズに応じてレイアウトを切り替えるにはそのままでは問題があります。というのも、iPadで Split View や Slide Over でアプリを動作させた時に、 Dimensions が常に画面全体のサイズを返してしまうからです:
<span id="05dd" class="pi pj io rv b gz rz sa m sb sc">console.log(Dimensions.get('screen')) // {fontScale: 1, width: 768, height: 1024, scale: 2}<br></br>console.log(Dimensions.get('window')) // {fontScale: 1, width: 768, height: 1024, scale: 2}</span>
知りたいのは画面全体のサイズではなくアプリ領域のサイズです。それを取得するには、アプリの最も外側に一枚Viewを敷いて、 flex: 1 をスタイルに指定します。そしてそのViewの onLayout イベントでビューのサイズを取得するのです。そのサイズをReduxのStoreなどに記憶しておきます。
こちらがコードのスニペットです。ご参考ください(英語):
アプリの完成度が高まるにつれて、必ずどこかでパフォーマンスの調整が必要になります。React NativeはReactでUIを描画しますので、Reactのパフォーマンス最適化手法がほぼそのまま使えます:
アプリを軽快に保つための基本的な手法としては、 shouldComponentUpdate() を使って無駄なレンダリングを阻止する方法です。さらにReact.PureComponent を使えば自動で props を監視して、それが変化したときだけレンダリングするように計らってくれます。僕は個人的に Higher-Order Components(HOC) パターン を採り入れているので、recompose の pure を使用して同様の事をしています。
PureComponentを使ってビューを構成していたとしても、気をつけるべき点があります。以下の例を見てみましょう:
<span id="1dbc" class="pi pj io rv b gz rz sa m sb sc">function CommentList(props) {<br></br> return (<br></br> <div><br></br> {props.comments.map((comment) => (<br></br> <Comment comment={comment} key={comment.id} onPress={() => props.handlePressCommentItem(comment)} /><br></br> ))}<br></br> </div><br></br> );<br></br>}</span>
この CommentList のレンダリング時に onPress プロパティに対してコールバック関数が渡されていますが、処理が実行されるたびに新しい関数が作られてしまっています。すると、 Comment がたとえPureComponentであっても、毎回異なるコールバック関数が渡されるので、レンダリングがスキップされずに処理が重くなってしまいます。この問題を避けるには以下のように記述します:
<span id="5864" class="pi pj io rv b gz rz sa m sb sc">function CommentList(props) {<br></br> return (<br></br> <div><br></br> {props.comments.map((comment) => (<br></br> <Comment comment={comment} key={comment.id} onPress={props.handlePressCommentItem} /><br></br> ))}<br></br> </div><br></br> );<br></br>}</span>
もしリストが長くなるようなら、FlatList を使用しましょう。

React Nativeで起動画面をセットアップした人は分かるかもしれませんが、React Nativeのアプリ実体であるJavaScriptコード本体の読み込み時に真っ白な画面が一瞬表示されます。特に白以外の背景を持つアプリにとっては強い違和感を与える現象です。
この問題の対処には以下の記事を参考にしました。この記事は上記画像のように、JSのロード中でも画面が真っ白になるのを防ぐ起動画面をセットアップする方法が丁寧に解説されています。めっちゃ有用です:
CodePush はアプリを審査を通さなくてもアップデートできるようにする仕組みです。CodePushを使えば、軽微なバグ修正などを素早く現行のアプリに適用出来ます。
しかしちょっと待ってください。まずApp Storeのレビューはもう既に充分短いです。昔は平気で2週間ほど待たされたものですが、今では平均で2日以内で審査が完了しています。よほど逼迫していない限り、ビジネスに大きな影響は無いでしょう。また、CodePushはネイティブ拡張を伴うライブラリです。先に述べたように、アプリを安定してシンプルで簡潔に保つためには出来るだけ使用を避けたいものです。
以上が、拙作アプリをよりよくするために工夫したことでした。参考になれば幸いです!
この記事をお読みくださりありがとうございます。僕はフリーランスをしながらアプリを作っていて、それだけで食っていこうと日々奮闘している者です。その過程をブログに書いていますので、ぜひ他の記事も読んでみてください!