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

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

そこでオススメしたいのが、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の将来のバージョンに適応しやすくします。

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 を使用しましょう。

JSロード中に真っ白な画面になるのを防ぐ

React Nativeで起動画面をセットアップした人は分かるかもしれませんが、React Nativeのアプリ実体であるJavaScriptコード本体の読み込み時に真っ白な画面が一瞬表示されます。特に白以外の背景を持つアプリにとっては強い違和感を与える現象です。

この問題の対処には以下の記事を参考にしました。この記事は上記画像のように、JSのロード中でも画面が真っ白になるのを防ぐ起動画面をセットアップする方法が丁寧に解説されています。めっちゃ有用です:

CodePush はアプリを審査を通さなくてもアップデートできるようにする仕組みです。CodePushを使えば、軽微なバグ修正などを素早く現行のアプリに適用出来ます。

しかしちょっと待ってください。まずApp Storeのレビューはもう既に充分短いです。昔は平気で2週間ほど待たされたものですが、今では平均で2日以内で審査が完了しています。よほど逼迫していない限り、ビジネスに大きな影響は無いでしょう。また、CodePushはネイティブ拡張を伴うライブラリです。先に述べたように、アプリを安定してシンプルで簡潔に保つためには出来るだけ使用を避けたいものです。

以上が、拙作アプリをよりよくするために工夫したことでした。参考になれば幸いです!

この記事をお読みくださりありがとうございます。僕はフリーランスをしながらアプリを作っていて、それだけで食っていこうと日々奮闘している者です。その過程をブログに書いていますので、ぜひ他の記事も読んでみてください!

Read more

過集中を避けるための働き方とルーティン(二児の父ver.)

過集中を避けるための働き方とルーティン(二児の父ver.)

どうもTAKUYAです。 先日書いた通り、最近個人開発を頑張りすぎて体を壊してしまいました。 その原因の一つが過集中癖です。自分はもともと何かに集中すると周りが見えなくなる傾向があり、それがたまに私生活にも影響を及ぼします。同じ失敗を繰り返さないためにも、ちょっと働き方を再設計したいと思います。 働き方に対して他人の指摘をアテにしない 自分のようなフリーランサーまたは自作サービスで生計を立てている人は、時間の使い方を自分で自由に決められます。その反面、どこまでも極端な働き方が出来てしまい、それを指摘したり止めてくれる人がいないという欠点もあります。自分には妻がいますが、全く違う業界なので自分の作業ペースがどのようなものか具体的に把握できません。 「疲れた!」と言えば「休んだら?」と言ってくれますが、働き方やペース配分などにまで口は出しません。なので、他人のストップサインはアテに出来ません。 (心理カウンセラーの可能性を別途検討中) 最近子供が生まれたので厳密なルーティン実行は出来ない 一日を時間単位・分単位で区切ってルーティンを組むのは気持ちがいいですよね。僕もそうしたい

By Takuya Matsuyama
なぜ体を壊してまで個人開発を頑張るのか?自尊心の欠如や過集中癖と向き合う

なぜ体を壊してまで個人開発を頑張るのか?自尊心の欠如や過集中癖と向き合う

どうもTAKUYAです。最近、個人開発を頑張りすぎて体調を崩してしまいました。アトピーが猛烈に悪化して、QoLが著しく下がってしまいました。まだ療養中ですが、毎日1万歩以上歩いて、徐々に回復しつつあります。 この過ちを繰り返さないためにも、自分は一体何が原因で頑張りすぎてしまうのか?という事について深堀りして考えてみたいと思います。また、個人開発におけるメンタルヘルスはあまり語られていないトピックだと思います。本記事が、同じように仕事を頑張りすぎてしまう人の助けになれば幸いです。 TL;DR * なんとなく続けていたソフト開発が自分を救った * 原体験が歪んだモチベーションを生んでしまった * 親が引くほどの過集中癖がある * 生得的な直せないバグと考えることにする * アプリの成功に関係なく、自分をあるがままに受け入れる * 挫折しないのは、なんだかんだで前向きだから * ユーザさんから「休め!」と叱咤された * 人生は長い。個人開発なんかで死ぬな 自己の原体験について振り返ってみる 個人開発だけで生活するようになって、かれこれ8年ぐらいが経ちます。こう

By Takuya Matsuyama
ユーザサポートの問い合わせを装った攻撃が怖すぎた

ユーザサポートの問い合わせを装った攻撃が怖すぎた

どうもTAKUYAです。個人開発をしていてアプリの知名度が上がってくると、作者個人(あるいはサイト管理人)を狙った攻撃というのをたまに受けます。つい先日も、怖すぎるメールを受け取ったのでシェアします。 件名: Cookie consent prevents platform access Hello, I cannot access use the store. The cookie consent notice keeps appearing and nothing happens once I approve or try to close it, so I’m unable to interact with the website. Please provide guidance on

By Takuya Matsuyama
万年ペーパーの自分が車の運転を楽しめるようになった理由

万年ペーパーの自分が車の運転を楽しめるようになった理由

どうもTAKUYAです。大学の入学前に免許を取って以来ずっとペーパードライバーで、都市生活では出来る限り運転は避ける生活を送っていた。事故を起こせば人を◯してしまう可能性もある代物を日常的に運転するなんて考えられなかった。 そんな自分に転機が訪れたのは、結婚して大阪に戻った事と、子供ができた事、そしてアウトドアに興味を持った事だ。大阪近辺だと箕面とか野勢、神戸、丹波篠山などが日帰りでドライブしやすい距離だ。それで、恐る恐るタイムズのカーシェアで時々ではあるが運転するようになった。 他の車も生きた人間が運転しているという驚き まず運転していて気づいたのは、他の車にも生きた人間が運転していると言う点だ。そんなのは当たり前だろと思うかもしれないが、結構新鮮な発見だった。Grand Theft Autoなどの現代をモチーフにしたゲームをプレイすれば分かるが、NPCの車の動きは鈍臭いのでガンガンぶつかる。プレイヤーの進行を予測した動きなどしないからだ。 しかし現実では相手も事故りたくないので、お互いに動きを読み合い、譲り合って運転する。ルードな運転手もたまにいるものの、どちらかがよっぽ

By Takuya Matsuyama