- はじめに
- Flutter を選んだ理由
- Flutter 設計の実例:アーキテクチャ選定と試行錯誤
- 設計の失敗例:ここでつまずいた…
- どう乗り越えたか:改善のプロセス
- 細かいけど開発中に思って今も考えていること
- チームに感謝 !!
- おわりに
はじめに
開発の背景や目的
2024年夏、私たちのチームは「新しい価値を生み出すチーム」として召集されました。
プロジェクトのメンバーは、サーバーエンジニア、Android エンジニア、iOS エンジニア、そしてデザイナーや QC、PdM といったメンバーが集まった少人数の精鋭チームです。
この新たなプロダクトの開発には、大きな期待と責任が伴っていました。
既存のプロダクトでは対応しきれていない業務領域がある中、それを解決するためのアプローチが必要でした。
ただし、目指すのは単なるツールではありません。
スマホアプリは現場のユーザーが日々使うものですから、機能性だけでなく、直感的で快適な操作性も求められます。
さらに、今回のプロジェクトは単に課題を解決するだけでなく、私たちの ANDPAD が新たなビジネスの可能性を切り開くための挑戦でもありました。
ユーザーに寄り添い、現場に最適化されたアプリを届けることで、新しい市場を作り出し、競争力を高めるのが私たちの目標でした。
以上の背景を踏まえ、技術的な挑戦やユーザー体験の設計プロセスを振り返ります。
設計段階で成功したこと、そしてつまずいたことを正直に共有することで、これからFlutterを用いてプロダクト開発を考えている方々に、少しでも参考にしていただければ幸いです。
ANDPAD についてはぜひこちらをご参考ください。
Flutter を選んだ理由
本プロジェクトでは、迅速な開発とユーザー満足度の高いプロダクト提供が求められました。そのためには効率的な開発体制と確かな品質の同時実現が不可欠です。
これらの要件を満たすソリューションとして、私たちは Flutter を選択しました。
Flutter を選んだ理由は大きく3つあります。
コードの共通化による効率化
Flutter を使えば、Android と iOS の両方のアプリを1つのコードベースで開発可能です。 この共通化により、開発速度が大幅に向上し、同時に UI や機能の一貫性も確保できます。
ホットリロードによる UI 確認の速さ
Flutter の特長の1つであるホットリロードは、開発中にリアルタイムで変更を確認できるため、UI デザインや動作を素早く試行錯誤できます。 これにより、チーム全体の開発スピードが飛躍的に向上しました。
スピードと品質を両立するテスト環境
Flutter は、ユニットテストからウィジェットテスト、さらには統合テストまで網羅できる充実したテスト環境を備えています。 このおかげで、短期間での開発ながら、品質をしっかりと担保することができました。
これらの特長により、Flutter は「スピードと品質を同時に実現する」というプロジェクトの課題に対して、理想的なソリューションとなりました。
Flutter 設計の実例:アーキテクチャ選定と試行錯誤
Flutter に加え、アンドパッド内製の認証ライブラリを活用し、 Flutter とネイティブコード間のスムーズな連携を確保しました。
これにより、セキュアかつ直感的なログイン体験を提供しています。
では、ここからは実際に Flutter でどのように設計・実装したのかを紹介します。
アーキテクチャの選定
様々な選定の方針になっているのが、今回の開発に求められていたスピートです。これを達成すべく、無駄なコードを生まず、チームの開発生産が上がることを方針にしました。
まず Flutter が前提にしている MVVM をアーキテクチャに採用しました。 また state 管理がしやすい Riverpod を採用し、可読性と保守性がよくなり、実装がしやすいをことを狙いました(Rivepodについては後述します)。
続いて採用したのが、軽量版 DDD(ドメイン駆動設計)です。アプリ開発をされている読者なら経験上、お分かりになると思いますが、アプリを素直に開発すると、Util クラスのようなものができ、肥大化して、同じような機能を同じようなコードを書いてしまいます。そこで、DDD にしたほうがビジネスロジックをまとめられるなど、コードが煩雑にならず、どこに何が書かれているかわかりやすくなります。 ただ、DDD そのものを採用するには重たいため、あえて崩して軽量版 DDD にしました。
- MVVM を採用し、UI と ViewModel を分離して実装。
- Model には軽量版 DDD を採用し、設計の一貫性を向上。
実際には以下のようなドメインモデル図の設計図を作成し開発を進めました。
リポジトリ設計と CQRS の活用
よく言われる通り、ドメイン知識は書き込み系に現れますが、それを 1 ファイルにすると、どこに何が書いてあるか見通しが悪くなり、行数も増えます。また、曖昧さもでてきて、たとえば "通信している" という中に get と query などが混在しがちです。そこで責務を分けて明確化するために get と query を分けて取り扱える CQRS を採用し、コードの読みやすさと保守性を高めました。
- Model の Repository 層で CQRS(Command Query Responsibility Segregation) を採用。
- Command(書き込み)と Query(読み取り)を分けることで、コードの可読性と責務の明確化を実現。
以下がその概要図です。
具体的には、作成したドメインモデル図からドメイン領域に適した Repository をグループ化し、そのグループ単位で Command Query のそれぞれのクラスを作成しています。
Riverpodを採用
今回のプロジェクトでは、画面の状態管理を効率的かつシンプルに行うために Riverpod を採用しました。
その理由は以下の通りです。
- 状態管理の明確化
- Riverpod は、状態を Provider として一元管理できるため、UI と状態の依存関係が明確 になります。これにより、画面ごとの状態が適切に分離され、コードの保守性が向上した
- 依存関係の管理が容易
- Riverpod は 依存関係の注入(Dependency Injection) をサポートしているため、ViewModel や Repositoryなどの依存関係を簡単に管理できます。これによりテストがしやすい環境を構築できた
- シンプルな状態の再構築
- Flutter では画面遷移やウィジェットの再構築時に状態の保持が課題となることがありますが、Riverpod では Provider のライフサイクル管理が組み込まれているため、状態の再構築を意識せずに済んだ
- スコープの柔軟性
- ScopedProvider や StateNotifierProvider などを利用することで、画面ごとに状態をスコープ化ができた
- 局所的な状態管理とアプリ全体の状態管理を柔軟に使い分けられるといった設計が可能になり、画面ごとの状態管理がシンプルかつ明快になった
コード生成ツールの活用
今回は API を書くのにあたって、バックエンド側で OpenAPI 仕様の YAML ファイルが定義されていました。そこでアプリ側では OpenAPI Generator を利用し、開発スピードを上げました。
- OpenAPI Generator を使用し、サーバーAPIのクライアントコードを自動生成
- 手書きコードを大幅に削減し、実装量を劇的に減少
ディレクトリ構成と責務の明確化
繰り返しになりますが、チームの開発生産性を上げるため、ディレクトリ構成も責務を明確しました。
実際には以下のようなディレクトリ構成となりました。
レイヤーごとの責務を明確化することで、テストコードも書きやすく、保守性の高い設計ができるようにしました。
1. lib/data/ - データ関連
model/
: データモデルを定義し、アプリのデータ構造を管理。repository/
: データ取得・保存を担当し、データソースとのやり取りを抽象化。
2. lib/foundation/ - 基盤機能
- 共通のユーティリティやロガー、設定ファイルを配置し、アプリ全体を支える機能を提供。
3. lib/router/ - ルーティング
- 画面遷移を一元管理し、コードの見通しと依存関係を整理。
4. lib/ui/ - UI関連
page/
: 各画面ごとのディレクトリ。関連するViewModelやWidgetを配置。theme/
: アプリのテーマやスタイルを管理。widget/
: 共通ウィジェット(例: ローディング、エラーメッセージ)を配置し、再利用性を向上。
UI 実装の工夫
ここでも無駄な実装を避け、チームの開発生産を上げるべる、全画面で使うもの、画面の中で共通に使うもの、個別のもので分けました。これによって無駄なコードを省きながら、どこに何があるかわかりやすくなりました。
- Page ごとに Widget を構成し、複雑な画面でも分割して管理
- 共通パーツは Widget として切り出し、ネストの深いコードを回避
- 理解しやすいコード設計により、UI の保守性を向上
概ねの全体像としては以下の通りです。
これにより各レイヤーの責務が明確になりました。
設計の失敗例:ここでつまずいた…
一方で、うまく設計したつもりでも、軽量版 DDD は難しく、以下のような課題が挙がりました。
- あえて軽量 DDD のように設計をしたが、過剰設計になってしまった印象があった
- 今回のケースでは、アプリ側にドメイン知識がほぼないにもかかわらず、Entity や ValueObject を過剰に設計してしまい、処理やコードが複雑化してしまった
- 今回のケースでは、サーバー側がドメイン知識を持つべきであることを認識できていなかった
- アプリ側で過剰にドメイン知識を持つことで、詳細設計時に無駄な処理や複雑なコードが増えてしまった
どう乗り越えたか:改善のプロセス
課題がわかってきたところで、主には Entity と ValueObject に注目し、以下のように解決しました。
- アプリ側の責務を削減
- ドメイン知識を持つべき箇所はサーバー側であるため、アプリ側の役割を削減するように見直し
- アプリ側は「表示・入力」の責務に集中し、サーバーから取得したデータの表示や操作にフォーカスするよう設計をシンプル化した
- 特に ValueObject と Entity に関してはドメイン貧血症を起こしてしまっていた
- ただ、この Entity / ValueObject があるおかげで型の安全性が担保され、UI 側でも送信するデータの誤りなどは発生しなかった
- 設計の見直し
- 過剰に設計された Entity や ValueObjec tを削減し、必要最小限のデータ構造にリファクタリング
- 設計見直しにより、アプリ側は表示・入力に特化し API 中心に変更。結果、責務が明確化され、シンプルで保守性の高い設計へと改善できた
- 無駄な処理の排除
- 詳細設計時に追加してしまった無駄な処理を洗い出し、必要のないロジックを削除
- シンプルな設計を保つことで、今後の機能追加時の影響範囲を最小限に抑えた
設計を見直した結果、アプリ側とサーバー側の役割分担が明確になり、シンプルかつ無駄のない設計に改善することができました。
細かいけど開発中に思って今も考えていること
とはいえ、まだ課題が残っていますので、今後も改善を進めていきます。
- ValueObject はボイラープレートで自動化したい
- Dart3.0 から Sealed クラスを使ったパターンマッチングができるので積極的に使うとコードが綺麗になる
- ListView より SliverList を使う
- テストコードはメンテできる範囲にして不要と判断したらテストコードも削除していく
チームに感謝 !!
ふりかえりとして、アンドパッドにジョインして2024年12月時点で 10ヶ月ですが、社内の風通しの良さ、相談のしやすさ、メンバーの支え合う姿勢に感謝しかなかったです。
また、アンドパッドのミッションである「幸せを築く人を幸せに」をそれぞれが開発する動機として持っているため、高めあう認め合う文化というものがアンドパッドにはあるのだと開発を通じて教訓を得ました。
また、アプリの設計に関しては詳細設計もそうですが、スマホアプリがUI表示のためのツールになっているとしたらそれに適用した設計とはどのような形なのかをANDPADなりの設計を追求していくとても良いきっかけになりました。
おわりに
プロダクトは完成がゴールではなく、新たな価値創造のスタート地点です。
私たちは CI/CD や E2E テスト強化、新機能の継続的な開発を進めながら、 "幸せを築く人を幸せに" という理念のもと、これからもさらなる品質向上と技術研鑽に励んでいきます
私たちと共に新たな価値を生み出していきませんか? アンドパッドでは、技術力を伸ばしたい、もっともっと人を幸せにするシステム開発をしたいというエンジニアを大募集しています! お気軽にカジュアル面談や選考に応募ください!