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

Inkdrop v6 Canary版リリースしました — 新Markdownエディタやその他新機能盛り沢山

Inkdrop v6 Canary版リリースしました — 新Markdownエディタやその他新機能盛り沢山

Inkdrop v6.0.0 Canary版リリースしました — 新Markdownエディタやその他新機能盛り沢山 こんにちはTAKUYAです。 v6.0.0 の最初の Canary バージョンをリリースしました 😆✨ v6では、アプリのコア機能の改善がたくさん盛り込まれています! * リリースノート(英語): https://forum.inkdrop.app/t/inkdrop-desktop-v6-0-0-canary-1/5339 CodeMirror 6 ベースの新しいエディタ フローティングツールバー v5ではツールバーがエディタの上部に固定されており、使っていないときもスペースを占有していました。 v6では、テキストを選択したときだけ表示されるフローティングツールバーに変わりました。 GitHub Alerts 構文のサポート Alerts の構文が正しい色と左ボーダーでハイライトされるようになりました。 ネストされたアラートや引用にも対応しています。 また、アラートタイプの入力を支援する補完機能も追加されました。 スラッシュコマンド 空行で /

By Takuya Matsuyama
AIのお陰で最近辛かった個人開発がまた楽しくなった

AIのお陰で最近辛かった個人開発がまた楽しくなった

AIのお陰で最近辛かった個人開発がまた楽しくなった こんにちは、TAKUYAです。日本語ではお久しぶりです。僕はInkdropというプレーンテキストのMarkdownノートアプリを、デスクトップとモバイル向けにマルチプラットフォームで提供するSaaSとして、かれこれ9年にわたり開発運営しています。 最近、その開発にClaude Codeを導入しました。エージェンティックコーディングを可能にするCLIのAIツールです。 最初の試行は失敗に終わったものの、徐々に自分のワークフローに馴染ませることができました。そして先日、アプリ開発がまた「楽しい」と感じられるようになったのです。これは予想外でした。 本稿では、自分がエージェンティック・コーディングをワークフローに取り入れた方法と、それが個人開発への視点をどう変えたかを共有します。 * 翻訳元記事(英語): Agentic coding made programming fun again 自分のアプリに技術的負債が山ほどあった ご想像のとおり、9年も続くサービスをメンテするのは本当に大変です。 初期の頃は新機能の追加も簡単で

By Takuya Matsuyama
個人開発を7年以上続けて分かった技術選択のコツ

個人開発を7年以上続けて分かった技術選択のコツ

個人開発を7年以上続けて分かった技術選択のコツ InkdropというMarkdownノートアプリを作り続けて7年になる。 お陰さまでその売上でずっと生活できている。 これまで個人開発でどう継続していくかについて「ユーザの退会理由をあれこれ考えない」とか「アプリの売上目標を立てるのをやめました」とか、ビジネス面あるいはメンタル面からいろいろ書いてきた。 今回は、技術面にフォーカスして、どう継続して開発していくかについてシェアしたい。 TL;DR * 最初はとにかく最速でリリースする事を最優先する * 迷ったら「ときめく方」を選べ * 程よいところで切り上げて開発を進める * 使っているモジュールがdeprecatedされるなんてザラだと覚悟する * 古いから悪いとは限らない * シンプルにしていく * 老舗から継続の秘訣を学ぶ * 運ゲー要素は排除しきれない 最初はとにかく最速でリリースする事を目標に技術選定する 開発計画とビジネス計画は切っても切り離せない。 コーディングに傾倒するあまり完璧主義に陥って結局リリース出来ないまま頓挫してしまう個人開発者は多い

By Takuya Matsuyama
子育て中の個人開発者の一日

子育て中の個人開発者の一日

子育て中の個人開発者の一日 どうもTAKUYAです。 久しぶりに生活まわりの事を書きたい。自分はInkdropというMarkdownノートアプリを売って生きている。 子供も無事順調に成長しており、あと数ヶ月で3歳になるというところで、イヤイヤ期もやっと終わりが見えてきた。 生活パターンもなんとなく定着しつつあるので、ここで一旦どんなルーティンなのか書き出してみる。ちなみに当方今年で40歳。 平日の1日の流れ * 06:30 妻と子供起床、朝食 * 07:10–30 俺起床、朝食 * 07:40 布団を畳んで子供を着替えさせる。妻はその間に化粧や通勤の準備 * 08:00 ストレッチと軽い筋トレ(腕立て50回、スクワット100回) * 08:10 妻と子供を見送る。15分前後瞑想 * 08:30 散歩 * 09:00 作業開始(カフェまたは家) * 11:00 昼飯 * 12:00 ダラダラする * 12:30 作業再開(だいたい家)

By Takuya Matsuyama