
2026/05/06 22:44
Our Car:家族向けのアプリを作成してみて学んだこと
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
「OurCar」の核心となる物語は、Flutter と Pocketbase を使用して実用的なアプリとして成功裏に構築されたにもかかわらず、計画された発射前に家族ユーザーが引っ越してしまい安定版へのリリースに至らなかったという点です。家庭内でガソリン代の分割負担を支援し、車両の使用状況を追跡およびスケジュール管理を行うために開発されました。この車共有ツールは、プライバシーを損なうことなく旅行ログを取ったり燃料残高を管理したりすることで、「WhatsApp グループレベルよりも優れている」と設計されています。重要な点として、設計段階では過度なバッテリー消耗を防ぎ、リアルタイムの位置情報共有に伴うユーザーデータの問題から保護するため、求められた GPS 追跡機能は拒否されました。開発プロセスには、複雑なステータスフローを単一のタイムライン状態機械に再構成したり、深リンク対応での初期不具合後にナビゲーションパッケージを切り替えたり、開発者アカウントと家族携帯電話のペアリングにより App Store の厳格な配信問題を解決したりするなど、大きな技術的課題を克服することでした。アプリ機能はユーザー管理及び招待フローを含めるように拡張されましたが、プロジェクトの寿命は生活環境の突然の変更によって急激に終焉しました。結局、「OurCar」は、実用的でよく設計されたソフトウェアプロジェクトでも、商業的な収益化や最終的な Ver. 1.0 リリースを達成する前に、現実世界のロジスティクスの変化がプロジェクトを停止することを示すユニークな事例研究として機能します。
本文
TL;DR: 家族との車共有を実現するためのアプリを Flutter で作成しました。非常に興味深い経験でした。
必要性が発明の母であること
キッチンテーブルを取り囲み、ガソリン代の分割計画を立てようと坐っていました。これは長年抱え続けていた課題であり、私が家族に加わったことで問題はさらに深刻化してしまいました。ガソリンタンクが空になった時点で誰が責任をどう負担するかを明確にする手段が必要でした。なぜなら、通常は誰かが車を運転した結果タンクが空になると、その人に代償の請求が降りるからです。これは明らかに不公平です。もちろん、私たちは燃料を大幅に消費した際には必ず満タンにすることを心がけていましたが、商店へ買い物に出掛けて往復した場合などは消費量が少なく「もう補充する必要はない」と感じるものです。それが積み重なって最終的にタンクが空になるという「一万のナイフによる死」的な状況だったのです。
最初に出てきたアイデアは甥姪から提案されました。「走行したキロ数に応じてガソリンを補充する」というものですが、車のディスプレイには残量表示(現在地までの平均燃費に基づいた推定値)があります。しかし、ここでの問題はガススタンドに「タンクに 100 キロ分だけ充填してくれ」と頼むことはできない点です。リッター単位での給油しかしてもらえないため、この計画は現実的ではありませんでした。
すると私は、「走行距離と燃費効率を合わせれば、その回の行程で消費したガソリン量を正確に計算できる」と気づきました。しかし他の家族成员がそんな手間をかけて毎回計算することに熱意を見せたわけではなかったので、別の解決策が必要となりました。結局のところ、誰もが自分が利用した際に車を満タンにするという方法に落ち着きましたが、これは少しは機能していたものの、ちょっとした用事だけを目的に車を借りた時などは非常に面倒でした。
私は長らくアプリ作成を欲していました。あるべきアイデアがあればすぐに着手できると考えていたのです。やがて、ここに解決すべき課題が銀の皿に乗せられたように現れました。車の共有に伴う他の問題も存在しましたが(誰が車を持っているか、どこに駐車しているか、予定の調整など)、ガソリンタンクが空になる問題は「駱駝の背に刺さった最後の一針」となりました。これらの課題の多くを解決できるものを作成できると考え、即座にプロトタイプ作製に取り掛かりました。
プロダクトの誕生
このプロジェクトのスコープを「WhatsApp グループよりも車共有に適したアプリ」と定義しました。具体的には以下の機能を備える必要がありました:
- 車の位置情報を知ること可能であること(利用可能な場合のみ。誰がどこに連れ去ったかは私の個人問題です)。
- 車のステータスを把握し、誰がいつ利用しているかを知らせられること(違反切符の追跡の際には非常に役立ちます)。
- ガソリンタンクの状況と各人の使用量を記録できること(どのくらい補充すべきかを明確にするため)。
他にも必要な機能はあるでしょうが、これらは上記の目標達成のための手段に過ぎず、それら一一つを羅列する必要は感じていませんでした。
既存アプリを探しましたが、当時そのようなものは存在しませんでした。企業向けには従業員が社用車の使用状況を管理したり、企業がフリートを運用するためのアプリなどはありますが、私たちが求めるような「友人や家族間で車を共有したい」というユースケースに適した良いものは何も見つかりませんでした。
このプロジェクトで使用した技術スタックは非常にシンプルです。PocketBase(バックエンド)と Flutter(フロントエンド)を採用しました。その後、Riverpod をステート管理用に追加し、Auto Route でナビゲーションやルーティングを実装していますが、詳細については後ほど述べます。これらを選んだ理由は、以前にもこれらの技術でプロジェクトを経験しており、私の目的には完璧に機能したからです。つまり、今回も問題なく使えるだろうと判断しました。そしてそれは正解でした。
プロジェクトを始めた当時(執筆時点では約 1 年前)、「エージェント型開発」は黎明期であり、構築作業に依存するほどの価値を見出せずにいました。ただし、トラブルシューティングや研究には不可欠なツールでした。その後、Claude Code がより有用になったため、エージェント型開発を試みるようになり、ある程度有効だと気づきました(最近ではさらに頻繁に利用しています)。
最初のプロトタイプは 1 週間で完成しました(ほぼフルタイムで作業しており、その間は求職中でした)。これには以下の基本的なページが含まれていました:
- メイン画面:現在の場所、オドメーター(走行距離)、推定タンク残量、車を「使用中」とマークするボタンを表示。
- 全行程リスト。
- 車の返却を記録するフォーム(位置、オドメーター、燃費効率を取得)。
- ガソリン充填履歴リスト。
- ガソリン給油時の入力フォーム(量と単価)。
- 特定の時刻に車で外出することを他のユーザーに通知するフォーム。
最後の 3 つは後から追加されました。このコア部分はほとんど変更されませんでした。いくつかの追加ページが作成されました(ユーザー管理、車両設定、その他小さなメニューなど)。その後、充填履歴と行程リストを統合した単一のタイムラインへと進化しましたが、全体としては引き続きシンプルです。
バックエンドでは、PocketBase に Webhook を追加し、前述のプッシュ通知送信を可能にしました。後にこの機能を使って多くの小機能を付加しました。「PocketBase での選択はこのプロジェクトにとって非常に良い決断だった」と言えます。
プロトタイプアプリケーションには当然ながら多くのバグや欠陥がありますが、クールで有望であり、市場適合性さえ持っていました(私の中では「神聖なるトリオ」)。したがって、ただこれら全てのユーザーのスマートフォンに届ける必要がありました。私は各種アプリストアへのアップロードと、ベータテスト機能による配布を選択しました。「それぞれの個人を訪問してスマホいじくり回す」という方法よりはるかに簡単だと(当時思っていました)判断したからです。しかしそれはそれほど愉快なプロセスではなかったのです。
特に、Apple の iOS ノタリゼーションという混乱を引き起こす仕組みを乗り越えるのに多大な時間を要しました。やがて妻の iPhone を私の開発者アカウントに登録し、この手順を簡略化することにしました。Google Play ストアへの公開は比較的容易でしたが、それほど大差はありませんでした。幸運なことに、ほとんどは一回限りの設定問題であり、今ではデプロイは大幅にスムーズになりました(なおも Apple がベータアプリを検閲する必要性を理解できていません)。最終的にアップロードが完了し、皆のメールアドレスを問い合わせればダウンロードできるようになりました。
この過程で、プロジェクト名を決めることも必要となりました。「com.mendelgreenberg.<ここに何を入れようか>」という appbundle 名で固まってほしくはありませんでした。フォルダ作成時「gastrax」と命名しましたが、少しは知的ですがあまり良い名前ではありませんでした。妻は正しく、「もっと良い名前を選ぶべきだ」と主張しました。彼女は ChatGPT と相談し、数多くの候補を吟味した(いくつかは既に存在していたものもあり)後、私たちは「OurCar」という名前を決定しました。間違いなくこちらの方が可愛らしいです。これで適切な名前がついたため、実際にアップロードして共有することができました。
すぐにフィードバックが寄せられました。
スコープの定義、あるいは「ノー」と言うこと
以下のようなアイデアや不満を受け取りました:
- 「メンテナンススケジュールをトラッキングできる?」
- 「入力が面倒すぎる。ダッシュボードを撮影して OCR で自動読み込ませてくれない?」
- 「GPS 追跡で入力なしで距離を測定すべき」
- 「ガソリン代はどうやって分割するの?」
- 「予定行程を設定できる機能はない?」
- 「使い方わからないよ」
その他など。
私が取り組むべきことと、改善が必要なことのどちらを選ぼうとするのが困難でした。さらに、何に取り組まないかを選ぶ必要も生じていました。完成させたいものについてのビジョンは明確だったため、多くのフィードバックは既に私のリストに含まれていましたが、それでもスコープ外となる提案や、「便利そうだが実際にはアプリを悪くしてしまうアンチパターン」として断るべきアイデアもありました。
良い例が GPS 追跡です。このアイディアは、移動中の位置情報を継続的に追跡し、車両の公称燃費と組み合わせて走行距離とガソリン消費量を算出するというものです。その後にアプリ上で可視化できる便利な機能です。しかしこのアイディアには 2 つの問題がありました。一つ目は、バッテリー消耗が甚大になる点(ライブ位置追跡はバッテリーを大量に消費します)。二つ目は、プライバシー侵害の懸念です。車の詳細や移動履歴(誰が、いつ、どれくらい利用したか)を共有する以上、そこに「どこまで連れて行ったか」を加えれば、兄嫁がデートに行ったタイミングなどが即座に判明してしまいます。実際には、車が再び利用可能になった際の駐車場所だけを知ればいいので、GPS はその用途に限って使用すれば問題ありませんでした。したがって、この機能は外すことにしました。
一方で、「ダッシュボード写真を撮って自動認識」といった正当な要望もありましたが、これら大抵はユーザー体験(UX)が重く不自然だったことが原因でした。画面設計上、iOS や Android のネイティブコンポーネントを使っているにもかかわらず、プラットフォーム固有の使い心地が欠けており、やや混乱を招いていたのです。
もしこの点を改善できれば、誰でも扱いやすくなり、回り道した workaround を求めることもなくなるだろうと判断しました。
日常アプリケーションのデザイン哲学
見た目の美しさや感覚については、「ネイティブらしさ」に徹しようという方針を早期から採りました。Flutter はどちらのプラットフォームでもネイティブの GUI フレームワークではありませんが、提供されるウィジェットはほぼあるいは完全にネイティブ toolkit に相当します。実際には、この方針によりアプリ独自のスタイルを持たず、ユーザーの手を煩わせることなく、プラットフォームと一体化したような感覚を持たせることにしました。また、アプリは非常に速く、直感的で使いやすくなることも望みました。
これを実現する上では様々な課題がありました。第一に、私はデザイン師ではありません。趣味があるように思っても、ゼロから始める以上は自らの限界を超えています。その結果、しばしば情報密度が高く、ナビゲーションが困難なウィジェットを構築し、何を改善すればよいのか頭を抱えていました。確かに実践が鍵だと言えます。時間の経過とともに、何が良い手法なのかの感覚が少しずつ磨かれてきています。
最初に直面した問題の一つ(私は唯一ではないと承知していますが)は、Flutter がプラットフォームごとに正しいウィジェットを使用する自動メソッドを提供していない点です。一般的には各ページごとに Material と Cupertino の 2 つのビューを分けて実装するとされています(Material は Android デザインシステム、Cupertino は Flutter による Apple デザインの実装)。これには大いに疑問を感じましたが、幸運にも「Flutter Platform Widgets」というパッケージがあり、ほぼ全てのウィジェットに対応した適応型バージョンを提供してくれました。しかしそれでも、Android と iOS のレイアウト慣習が大きく異なる領域が多く、柔軟に対処するために複雑なスパゲッティコードを書く必要がありました。まだコードをより明確に整理・抽象化する手法を見つけられていませんが、FPW(または類似のアプローチ)に依存するよりも自らのコードを改善すべきだったかもしれないとも考えられます。実用上は、Flutter が特定の設計アプローチを推奨しておらず、この問題を解決しやすくしていないため、仕方ない状況にあります。
また、「ネイティブらしさ」を感じさせる良いサンプルアプリを見つけるのが非常に困難でした。特にダッシュボード、リスト、フォームについては多数の実装方法がありますが、本当に優れたものは僅かです。Apple の Human Interface Guidelines や Material デザインシステムのガイドラインは指針が明確ではなく、特定のシナリオでのスタイル使い方の指導が少ないためです。調査を重ねた結果、WhatsApp が同じことを達成しようとしており、その限りにおいて最も成功した例だと考えます。両プラットフォームともに「ネイティブらしさ」を体現する最高峰のアプリと言えます。したがって、デザインの多くの判断は WhatsApp の実装を参考にしてきました。現時点でこそ、このような細かい配慮が統一感を生み出していると感じています。
また、特定の機能では他のネイティブアプリも参考にしました。とりわけ iOS のカレンダーアプリからフォーム設計を学んだ点は記憶に新しいです。
これらの一貫性を高める改善に取り組むにつれ、自分自身でのアプリ利用を楽しめるようになり、周囲からの「面倒だ」という苦情が減ってきました。最近では『日常Thingsのデザイン』を読み始めました。Don Norman はこの本を通じて繰り返し指摘していますが、「標準的な慣習に従えば、人は突然システムの仕組みを理解します。なぜならそのようなインタラクションを過去に何度も経験してきたからです」。
コード書き込み
初期バグ修正の過程で、いくつかの似たような問題に直面しました。「行程」を開始できるのに、メインダッシュボードが行程タイマーを表示するのはページを手動でリロードするまででした。複数のページ間でステート同期を実現する方法を検索した結果、この問題は自分だけではないことがわかりました。基本的には全てのデータがサーバー側で管理されているため、ユーザーが行った変更の直後にアプリを強制的に更新する必要がありました。解決策として Riverpod を採用しました。これはコード生成と「魔法」によって、単一のソースからステートを派生する複数のページの同期を管理するライブラリです。まだいくつか同期バグは残っていましたが、少なくとも健全なステート管理機構が実装された後では追跡しやすくなりました。
また、常にパフォーマンスの向上に注力しました。Flutter はかつて「ジャंक(つまずき)」で有名でしたが、近年多くの根本的な問題が解決しています(新しい Impeller レンダラーなど)。現代では開発者が良質なコードを書かないことが主な原因です。私の場合も同様で、巨大なメガウィジェットを作りすぎており、まだ整理分割すべき箇所が多いのが現状です。
これらが招く主要な問題は、ステートの少しの変化だけで多数のリペイントが発生してしまうことです。そのウィジェットが大きな共有ステートを持っているためです。React と非常に似たモデルを採用しており、親のステート変化で全体(子要素を含む)が再描画される仕組みです(ただしキャッシュ機能が働いていると信じています)。このため、些細な変更でも重大なパフォーマンス影響を及ぼします。逆に言えば、これを修正するのは比較的容易です。小さな複数のステートを保有する独立したウィジェットに分ければ済みます。なぜこれを行わなかったのかと言えば、再描画を防ぐための適切なステート分割方法を見出すのが思ったほど簡単ではなく、じっくりと精神的モデルを検討する時間を取らなかったためです。その結果、特定のフォームでの文字入力だけが遅くなっています。
これはスケールできない
開発の過程で、車のステータスを追跡するためのデータモデルと格闘しました。かなり複雑で、正しい動作のために複数の場所でデータを更新する必要がありました。同時に、「燃料ログ」と「行程ログ」を別々のページとしていることは非常に分かりにくい(いつ何が起きたかを把握しにくい)という苦情も寄せられ、この問題もある程度認識していました。そこで両方のログを一つの統合タイムラインに統合することでこれらの課題を一括解決できると考えました。
私の実装方法は、コードが車の現在のステート判定のために参照するログ(一種の状態機械)への切り替えでした。要約すると、車のステートに応じて実行可能なアクションが決まります:
- 車が「返却済み」であれば取り出すことができる。
- 逆に、車が「使用中」であれば返却できる。
- したがって、既に使用中であれば取り出せない。
他にもありますが、このくらいです。つまり、行程や充填を個別ログで管理しつつ、それらによって車のステートがどのように変化したかをまた別ログに記録するという方式を放棄し、全てを一覧ログで一元管理することにしました。これが更新処理を大幅に簡素化します。
しかし、思っていたほど簡単ではなかったのです。この実装の設計と構築には多くの時間を要し、当初は多数のバグを持っていました。特に、実装構造を工夫して速度低下や不便さを避ける方法を模索する過程で困難な面がありました。
正直言って、現時点での実装はまだ完璧ではなく、効率化のためにいくつかの手抜き(クロージ)を実装しています。アイデア自体は素晴らしいのですが、実行面では改善の余地があります。もし再構築すれば(ある時点で書き換えるかもしれません)、データに依存しない「抽象的な状態機械」を採用するコード構造をとるでしょう。現在の実装はログの保存方法と強く結びついており、デバッグが困難でした。抽象化することで、状態機械はデータの保存方法を気にせず、必要なデータポイントを問い合わせ、現在のステートに応じて変換を行うようにできます。これにより厳密にテストすることが可能となり、実データに触れる前にほとんど全てのバグ(場合によっては全て)を発見できるようになります。
同時に、このアプリを公開ベータ版へ移行し、Google や Apple が要求する要件(スクリーンショットや説明文、プライバシーポリシー、アカウント削除ポータルなど)を整える作業も進めていました。アカウント削除ポータルはエージェントを使って構築しており、非常に成功した取り組みでした。ユーザー管理機能など、私の積極的関与なしでも他者がアプリを利用できる機能も追加しました。これにより、特定の車両へのユーザー招待や除去が可能になりました。これまでのところ、バックエンドで手動でユーザーを追加する必要があり(車両自体も手動で作成)、これはスケール性が高くない問題でした。
これを修正するためには、新しいユーザーを車両に招待するフローを実装する必要がありました。コンセプトは以下の通り:ユーザーを招待するとメールが届き、リンクをクリックするとアプリ内で招待画面が開きます。これを実現するには、電話番号 OS が該当リンクをアプリで開くように指示するためのディープリンキング設定と、実際にナビゲーション機能への追加が必要でした。
これまでではポシュとポップ呼び出しが至る所にあり、ステートパスが混乱していました。したがって、複雑なハンドリングを自作したくない場合、正式にナビゲーションルーターを実装する必要があります。なぜかこの作業は容易ではありませんでした。複数のパッケージが存在し、それぞれ異なる実装手法を提供していますが、すべてドキュメントが不透明で、ナビゲーションの正しいモデルを理解するのに役立ちませんでした。数々の選択肢を試した結果、Auto Route に落ち着きました。しかしデバッグ戦に多くの時間を費やし、やがて単に実装方法を修正すれば「より適切」になることに気づいた時にはじめて進めました。プラス面としては、ルーターを導入することで長年構築してきた複雑なナビゲーションロジックが劇的に簡素化しました(最初からやるべきだったと学びました)。
これからの方向性
自分の目標を達成したアプリを開発できたと考えます。100% に達しているわけではありませんし、改善すべき箇所は多数あります。それでも全体としては心地よく使い続けられ、周囲も嫌悪していませんでした。
以前少し触れましたが、ユーザー増強や次のステップへ準備を進めようとしていましたが、他のユーザーを探すのが困難でした(正直言ってそこまで努力はしませんでした)。2.0 版での追加機能や将来の収益化についても計画していましたが、プロジェクトがその段階に達する前に、家族との引っ越しが決まり、車共有をしていた姪舅方全員も同時に遠くへ移住しました。そのためこのプロジェクトへの熱意が急速に冷えてしまいました。少なくとももう一つの興味を持つグループは見つけられましたが、それだけでは十分ではないと感じています。
将来的には再び必要になるか、あるいは必要な人を見つけることができ、1.0 版を達成できるかもしれないと願っています。ただ現時点では必要性がないため、アプリは不確定な状態のままアルファ版で永遠に保留されています。
この投稿のレビューや改善提案をご提供いただいた Manu および Berel 様へ感謝申し上げます。初期草稿は Claude にレビューしてもらいました。すべての執筆は私の独自のもの입니다。