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

Keychron K2 HEを無刻印化する手順

Keychron K2 HEを無刻印化する手順

どうもTAKUYAです。KeychronさんにK2 HEをお願いしたら音速で送ってくれたので、無刻印化してみました。どうやったのか過程をシェアします。 Unboxing 上はKeychron Q1です。これは3年間ぐらい使ってきました。キーキャップが若干くたびれていますね。でも問題なく今まで使えていました。そろそろ飽きてきたので新しいキーボードを試したいと思い、前から気になっていたK2 HEを試すことにしました(写真下)。 Amazon | 【国内正規品】Keychron K2 HE ラピッドトリガー ワイヤレス カスタムキーボード、ホールエフェクトGateronダブルレール・マグネットスイッチ、2.4GHz・Bluetooth無線対応、QMKプログラム可能、アルミ+ウッドフレーム、USレイアウト、RGBライト、Mac Windows Linux対応 (ブラック) | Keychron | パソコン用キーボード 通販【国内正規品】Keychron K2 HE ラピッドトリガー ワイヤレス カスタムキーボード、ホールエフェクトGateronダブルレール・マグネットスイッチ、

By Takuya Matsuyama
ノート駆動AIコーディング術の提案

ノート駆動AIコーディング術の提案

どうもTAKUYAです。みなさんはAIエージェントを普段のコーディングで活用されていますか。ちょっと面白いワークフローを思いついたのでシェアします。それは、ノート駆動のエージェンティック・コーディング・ワークフローです。最近Claude Codeのプランモードを使っていたら、ターミナル内で生成されたプランを読むのが辛かったんです。それで、じゃあMarkdownノートアプリであるInkdropをプランの保存先バックエンドとして使えば解決するんじゃないかと思って、 試してみました。こちらがそのデモです(英語): こちらがClaude Codeの設定ファイル群です: GitHub - inkdropapp/note-driven-agentic-coding-workflow at devas.lifeComplete Claude Code configuration collection - agents, skills, hooks, commands, rules, MCPs. Battle-tested configs from an Anthropic hackathon w

By Takuya Matsuyama
2025年個人開発活動の振り返り

2025年個人開発活動の振り返り

どうもTAKUYAです。もう1月も半ばに差し掛かっているけど、2025年の自分の活動の振り返りをしたい。去年を一言で言うなら、本厄を満喫した年だった。 厄年とは、人生の節目にあたって、体調不良や災難が起こりやすいと経験的に言われる年齢のこと。数え年で42歳、確かにもう若さに任せた事は出来ないなと痛感した年だった。(ところであなたの国ではこのような年はありますか?) 夏に体調を崩して2~3ヶ月動けなくなった 暖かくなり花粉が飛び出した頃に、持病のアトピーが悪化しだして、まともに生活出来なくなってしまった。酷さで言うと、2019年に脱ステした時と同じぐらい。 脱ステに無事成功したから、この地獄は二度と味わうことはないだろうと高を括っていたが、まさか7年後にまた味わうとは思わなかった。当時の独身時代と違い、妻も子供もいる中で、周りに多大な迷惑をかける事となった。夏の子供との思い出が全く無い。悲しい。 現在はQoLもほとんど元の状態まで復活できた。写真を撮って症状の変化を記録したので、機会があればシェアしたい。食事療法など色々試したが、結局歩くのが一番自分に効いた。それ以来、一日一万歩

By Takuya Matsuyama
書いて、歩け!なぜノートアプリはシンプルで充分なのか

書いて、歩け!なぜノートアプリはシンプルで充分なのか

どうもTAKUYAです。今回はノートやメモから新しい発想を生むための考え方についてシェアします。 自分はシンプルさをウリにした開発者向けのMarkdownアプリInkdropを作っています。なので、どうしても「ノートアプリの作者」としてのポジショントークが含まれてしまいますが、逆に言えば、「ノートアプリを約10年間作り続けてきた人間が、どうやってアイデアを生み出しているのか」 という実際的な体験談として読んでもらえれば幸いです。 結論から言うと、僕は「アプリ上でノート同士を連携させる必要はない。繋げるのはあなたの脳だ」と考えています。本稿では、ノートアプリの機能に溺れずユニークなアイデアを考え出すために僕が実践している事をシェアします。 TL;DR * ノート整理に時間をかけるな。グループ化で充分だ * すごい人はアイデアが「降りてくる」のを待つ * プログラミング × 料理動画 という有機的な掛け合わせ * ノートは「忘れる」ために書く * 歩け! ノート整理に時間をかけるな。グループ化で充分だ 巷ではZettelkastenなどが流行っているようですね。これ

By Takuya Matsuyama