Flutterプロジェクト構成とは?lib・pubspec.yaml・android・ios・test・assetsを徹底解説
Flutterは、単一のコードベースからモバイル、Web、デスクトップ、組み込み向けのアプリを開発できるオープンソースフレームワークです。Flutter公式サイトでも、Flutterは単一コードベースからネイティブコンパイルされたマルチプラットフォームアプリを構築できるフレームワークとして説明されています。Flutterプロジェクトを作成すると、lib、pubspec.yaml、android、ios、test、assets などの構成が用意され、Dartで書く共通コードと各プラットフォーム固有の設定が1つのプロジェクト内に共存します。
Flutterは「一つのコードで複数プラットフォームに対応できる」という点が魅力ですが、プロジェクト構成を理解しないまま開発を進めると、アプリが大きくなった段階で混乱しやすくなります。たとえば、画面コードをどこに置くべきか、状態管理をどこに分けるべきか、API通信やRepositoryをどの階層に置くべきか、画像やフォントをどう管理するべきか、AndroidやiOS固有設定をどこで変更するべきかが曖昧になります。Flutterは共通コードで多くを実装できますが、実務ではAndroidとiOSのネイティブ設定も避けて通れません。
本記事では、Flutterプロジェクト構成を20の大きな観点に分けて詳しく解説します。初心者が最初に理解すべき lib、main.dart、pubspec.yaml、android、ios、test の役割から、実務で重要になるassets、fonts、flavors、状態管理、Clean Architecture、packages、plugins、platform channels、CI/CD、セキュリティ、長期保守しやすい設計まで、Flutter開発で役立つ構成の考え方を体系的に整理します。
1. Flutterプロジェクト構成の全体像
Flutterプロジェクト構成を理解する第一歩は、Flutterアプリが「Dartコードだけ」で成立しているわけではないことを知ることです。Flutterでは、多くのUIやロジックを lib 配下のDartコードで実装できますが、AndroidやiOSのビルド、権限、署名、通知、Deep Link、Firebaseなどでは、プラットフォーム固有の設定も関わります。そのため、Flutterプロジェクトは共通コードとネイティブ設定が組み合わさった構成として理解する必要があります。
1.1 Flutterプロジェクトは共通コードとネイティブ設定を持つ
Flutterプロジェクトでは、アプリの主要なUI、状態管理、ビジネスロジック、API通信、データ変換などをDartで書きます。その中心になるのが lib フォルダです。一方で、android と ios フォルダには、それぞれAndroid StudioやXcodeで扱われるネイティブプロジェクト設定が含まれます。Flutterは共通コードで多くの処理を実装できますが、AndroidのManifestやGradle、iOSのInfo.plistやRunner.xcodeprojが不要になるわけではありません。実務では、Flutter側のコードとネイティブ側の設定がどのように連携するかを理解することが重要です。
1.2 libはFlutterアプリ本体の中心になる
lib フォルダは、Flutterアプリの本体となるDartコードを置く場所です。画面、ウィジェット、状態管理、モデル、サービス、Repository、UseCase、ルーティングなど、アプリ固有の多くのコードはここに配置されます。初心者は最初に lib/main.dart だけを編集することが多いですが、アプリが大きくなると、すべてを main.dart に書く構成はすぐに限界を迎えます。実務では、lib 配下を features、core、shared、presentation、data、domain などに分け、責務を明確にすることが保守性に直結します。
1.3 pubspec.yamlはプロジェクト情報の中心になる
Flutterプロジェクトのルートには pubspec.yaml があります。Flutter公式ドキュメントでは、すべてのFlutterプロジェクトに pubspec.yaml が含まれ、プロジェクトツリーの最上位に置かれ、DartとFlutterツールが必要とするメタデータを含むと説明されています。依存パッケージ、assets、fonts、Flutter SDK制約などもこのファイルで管理されます。 pubspec.yaml のインデントや記述ミスはビルドエラーにつながりやすいため、Flutter開発では非常に重要な設定ファイルとして扱う必要があります。
1.4 androidとiosはネイティブ連携の入口になる
Flutterアプリでも、AndroidやiOS固有の設定を変更する場面は多くあります。Androidでは AndroidManifest.xml、Gradle設定、minSdk、署名、Firebase設定などを扱い、iOSでは Info.plist、Xcode Target、Capabilities、CocoaPodsやSwift Package Manager、署名設定などを扱います。Flutterプラグインを追加したときも、ネイティブ側の設定が必要になることがあります。Flutterを使うと共通コードは増えますが、ストア公開や端末機能連携を行うなら、android と ios フォルダの基本構造を理解することが重要です。
1.5 testとintegration_testは品質保証を支える
Flutterプロジェクトでは、test フォルダにUnit TestやWidget Testを置き、必要に応じて integration_test に統合テストを置きます。Flutter公式のテスト概要では、よくテストされたアプリには多くのUnit TestとWidget Testがあり、重要なユースケースをカバーする十分なIntegration Testがあると説明されています。 テスト構成を早い段階で整えておくと、機能追加やリファクタリングを安全に進めやすくなります。FlutterはUIをDartで書けるため、Widget Testが特に強力です。
1.6 構成理解はマルチプラットフォーム開発に直結する
Flutterプロジェクト構成を理解することは、単にフォルダの名前を覚えることではありません。Dartコード、ネイティブ設定、依存関係、assets、テスト、flavors、CI/CDがどのように連携して最終的なアプリになるのかを理解することです。構成を理解しているチームは、Androidだけで起きる問題、iOSだけで起きる問題、Flutter共通コードで起きる問題を切り分けやすくなります。Flutterはマルチプラットフォーム開発を効率化しますが、その効率を最大化するにはプロジェクト構成の理解が不可欠です。
2. libフォルダの役割
lib フォルダはFlutterアプリの中心です。Dartで書く画面、状態管理、ビジネスロジック、データ取得、ルーティング、共通ウィジェットの多くはここに配置されます。Flutterプロジェクトの品質は、lib 配下をどれだけ整理できるかに大きく左右されます。
2.1 libはDartコードの中心になる
Flutterアプリの主要な実装は、基本的に lib フォルダに置かれます。画面を構成するWidget、状態を管理するControllerやNotifier、API通信を扱うService、データを表すModel、業務処理を表すUseCaseなど、多くのコードがここに集まります。小さなサンプルでは main.dart だけで完結しますが、実務アプリでは画面数や機能数が増えるため、lib の整理が必須になります。lib 配下が乱れると、ファイルを探す時間が増え、同じような処理が重複し、修正時の影響範囲も分かりにくくなります。
2.2 main.dartはアプリ起動の入口になる
lib/main.dart は、Flutterアプリの起動入口です。通常はここで runApp() を呼び出し、アプリ全体のルートWidgetを起動します。テーマ、ルーティング、依存注入、Firebase初期化、エラーハンドリングなどをここで設定することがありますが、すべてを main.dart に詰め込むのは避けるべきです。実務では、main.dart はできるだけ薄くし、アプリ初期化は app.dart、依存注入は injection.dart、ルーティングは router.dart のように分ける構成が使われます。入口ファイルを薄くすると、アプリ全体の見通しがよくなります。
2.3 app.dartで全体構成をまとめる
実務では、main.dart から App Widgetを呼び出し、app.dart に全体テーマ、ルーティング、ローカライズ、状態管理Providerなどをまとめることがあります。この分け方をすると、起動処理とアプリ構成を分離でき、main.dart が複雑になりにくくなります。たとえば、main.dart はFlutter bindingやFirebase初期化だけを担当し、app.dart はMaterialAppやGoRouter、ThemeDataを担当する形です。構成を分けることで、テストや環境別起動も扱いやすくなります。大規模アプリほど、入口処理とアプリ設定の分離が重要になります。
2.4 featuresで機能別に整理する
lib/features は、機能別構成でよく使われるフォルダです。ログイン、ホーム、検索、プロフィール、設定、通知など、機能ごとに画面、状態管理、Repository、UseCase、Modelをまとめます。この構成のメリットは、ある機能に関係するファイルを一箇所で探しやすいことです。大規模アプリや複数人開発では、機能ごとに担当範囲を分けやすくなります。ただし、共通処理まで各featureに置くと重複が増えるため、複数機能で使う処理は core や shared へ分ける必要があります。
2.5 coreは共通基盤を管理する
lib/core は、アプリ全体で使う共通基盤を置く場所としてよく使われます。ネットワーク基盤、エラー処理、定数、ルーティング、テーマ、ログ、DI、共通UseCase、共通Exceptionなどが候補になります。ただし、core は便利すぎるため、何でも入れる場所になる危険があります。core が巨大化すると、すべての機能がcoreに依存し、変更影響が広がります。coreに置くべきものは、本当にアプリ全体で使うもの、特定機能に依存しないものに限定するのが理想です。
2.6 sharedは再利用UIや共通部品に使う
lib/shared や lib/common には、複数画面で使う共通Widget、共通Model、共通Formatter、共通Validatorなどを置くことがあります。たとえば、PrimaryButton、AppTextField、LoadingView、EmptyStateView、ErrorBannerなどが候補です。sharedを使うとUIの重複を減らせますが、抽象化しすぎると柔軟性が下がる場合があります。最初からすべてを共通化するのではなく、実際に複数箇所で使われ、意味や見た目が安定しているものからsharedへ切り出すのが現実的です。共通化は保守性を高めますが、過剰な共通化は逆に複雑さを増やします。
3. main.dartと起動処理
main.dart はFlutterアプリの起動入口であり、プロジェクト構成の中でも特に重要です。起動処理を整理しておくことで、初期化、環境切り替え、エラーハンドリング、テスト起動が扱いやすくなります。
3.1 main.dartはできるだけ薄くする
main.dart はアプリの入口ですが、すべての処理を詰め込む場所ではありません。Firebase初期化、依存注入、環境設定、ログ設定、エラーハンドリング、runApp、ルーティング設定をすべて1ファイルに書くと、起動処理が読みにくくなります。実務では、main.dart は起動に必要な最小限の処理だけにし、複雑な初期化は別ファイルへ分けることが多いです。たとえば、bootstrap.dart に共通初期化、app.dart にアプリWidget、flavor_config.dart に環境設定を分けると見通しがよくなります。
3.2 runApp前の初期化を整理する
Flutterアプリでは、runApp() の前に初期化が必要な処理があります。たとえば、Flutter bindingの初期化、Firebaseの初期化、ローカルストレージの準備、環境設定の読み込み、エラー監視サービスの初期化などです。これらを無秩序に main.dart へ並べると、初期化順序が分かりにくくなります。実務では、初期化処理を関数化し、失敗時の扱いも明確にすることが重要です。起動処理はアプリ全体の安定性に関わるため、読みやすくテストしやすい構成にしておくべきです。
3.3 環境別mainを用意する場合もある
Flutterでは、開発用、本番用、ステージング用に main_dev.dart、main_staging.dart、main_prod.dart のような入口ファイルを分けることがあります。これにより、環境ごとにAPI URL、アプリ名、ログ設定、機能フラグを切り替えやすくなります。ただし、mainファイルを分けすぎると、共通初期化の重複が発生しやすくなります。共通処理は bootstrap.dart などにまとめ、mainファイルは環境設定を渡すだけにすると保守しやすくなります。環境別mainは便利ですが、重複を減らす設計が必要です。
3.4 エラーハンドリングを起動時に設定する
Flutterアプリでは、未処理エラーや非同期エラーを適切に捕捉するため、起動時にエラーハンドリングを設定することがあります。クラッシュレポートサービスを使う場合、FlutterErrorやPlatformDispatcherのエラー処理を設定することがあります。これらの処理を起動時に整理しておくと、本番環境での障害調査がしやすくなります。ただし、debug環境では開発者が問題に気づきやすいようにし、release環境ではユーザー影響を記録できるようにするなど、環境別の考慮も必要です。エラー監視はプロジェクト構成の重要な一部です。
3.5 依存注入の入口を分ける
大規模Flutterアプリでは、Repository、Service、UseCase、State Notifierなどの依存関係を手動またはDIライブラリで管理することがあります。依存注入の設定を main.dart に直接書くと、起動処理が肥大化します。injection.dart や service_locator.dart のような専用ファイルへ分けると、依存関係の登録場所が明確になります。テストでは本番実装ではなくFake実装へ差し替えることもあるため、DI構成はテスト容易性にも影響します。依存関係はアプリ全体の構造を決めるため、入口を明確にすることが大切です。
3.6 起動処理はテストやCIも意識する
main.dart の構成は、テストやCIにも影響します。起動時に外部サービスへ直接接続する処理が多いと、Widget TestやIntegration Testが不安定になる場合があります。テスト用の起動入口やMock設定を用意しておくと、テストが書きやすくなります。たとえば、App Widgetを直接テストできるようにし、外部依存はProviderやDIで差し替えられる構成が有効です。起動処理は実行時だけでなく、テスト時にも扱いやすい形にしておくことが重要です。
4. pubspec.yamlの役割
pubspec.yaml はFlutterプロジェクトの中心的な設定ファイルです。依存関係、assets、fonts、プロジェクト情報、Flutter SDK設定などを管理し、DartとFlutterのツールが参照します。
4.1 pubspec.yamlはプロジェクトメタデータを持つ
Flutter公式ドキュメントでは、pubspec.yaml はFlutterプロジェクトのルートに置かれ、DartとFlutterツールが必要とするメタデータを含むと説明されています。依存関係、assets、fonts、Flutter SDK制約など、プロジェクトの実行やビルドに必要な情報がここに集まります。 YAML形式は人間が読みやすい一方で、インデントに厳しいため、空白や階層を間違えるとエラーになります。Flutter初心者がよくつまずく場所でもあるため、pubspec.yaml は慎重に編集する必要があります。
4.2 dependenciesは実行時依存を管理する
dependencies には、アプリ実行時に必要なパッケージを指定します。HTTP通信、状態管理、ルーティング、Firebase、ローカル保存、画像表示、国際化など、アプリ本体が利用するパッケージはここに入ります。Flutter公式のパッケージ利用ドキュメントでも、Dart packageは最低限 pubspec.yaml を含み、依存関係やDartライブラリ、アプリ、リソース、テスト、画像、フォントなどを含められると説明されています。 依存関係は便利ですが、増やしすぎるとアップデート、脆弱性対応、ビルド時間に影響するため、導入理由を明確にすることが重要です。
4.3 dev_dependenciesは開発用依存を管理する
dev_dependencies には、テスト、Lint、コード生成、ビルド補助など、開発時だけ必要なパッケージを指定します。たとえば、flutter_test、build_runner、mocktail、freezed、json_serializable などが入ることがあります。アプリ実行時に不要なパッケージをdependenciesへ入れると、依存関係が不必要に広がります。実務では、実行時に必要なものと開発時だけ必要なものを明確に分けることで、プロジェクトの意図が分かりやすくなります。依存関係の分類は、小さなことに見えて保守性に影響します。
4.4 assetsはpubspecで登録する
Flutterでは、画像やJSONファイルなどのassetsを使う場合、pubspec.yaml に登録する必要があります。Flutter公式ドキュメントでは、Flutterはプロジェクトルートの pubspec.yaml を使って、アプリに必要なassetsを識別すると説明されています。 assetsを登録し忘れると、コード上では正しいパスを書いていても実行時に読み込めません。実務では、assets/images、assets/icons、assets/json、assets/lottie のように用途ごとにフォルダを分け、pubspecへの登録も整理しておくと管理しやすくなります。
4.5 fontsもpubspecで宣言する
カスタムフォントを使う場合も、pubspec.yaml でフォントファイルを宣言します。Flutter公式のフォント利用ドキュメントでは、フォントファイルをプロジェクトへ追加し、pubspec.yaml の flutter セクションで定義し、必要なweightやstyleを指定する方法が説明されています。 フォント設定はUI全体の見た目に影響するため、ファイル名、weight、styleの対応を正しく管理する必要があります。フォントを直接画面ごとに指定するより、ThemeDataやDesign Systemで統一的に扱うほうが保守しやすくなります。
4.6 pubspec.lockも理解する
Flutterアプリでは、依存関係の解決結果として pubspec.lock が生成されます。これは、実際に使われるパッケージのバージョンを固定するために重要です。アプリプロジェクトでは、チームやCIが同じ依存バージョンを使えるように pubspec.lock をGit管理することが一般的です。依存関係のバージョンが開発者ごとに変わると、ローカルでは動くのにCIで失敗する、ある端末だけ挙動が違うといった問題が起こります。依存関係の再現性は、安定したFlutter開発に欠かせません。
5. androidフォルダの構成
android フォルダは、FlutterアプリのAndroid側ネイティブプロジェクトを管理する場所です。Flutterコードだけでは解決できないAndroid固有設定を扱うため、基本構造を理解しておく必要があります。
5.1 androidフォルダはAndroidネイティブの入口
Flutterプロジェクトの android フォルダには、Gradle設定、AndroidManifest、KotlinまたはJavaコード、リソース、署名設定などが含まれます。通常のFlutter画面やロジックはDartで書きますが、Android固有機能を使う場合やビルド設定を変更する場合は、このフォルダを編集します。たとえば、カメラ、位置情報、Bluetooth、通知、Firebase、Deep Link、アプリアイコン、minSdk、署名などはAndroid側設定が関係します。Flutter開発者でも、Androidフォルダを完全に無視することはできません。
5.2 AndroidManifest.xmlは権限やActivityを管理する
Android側の AndroidManifest.xml には、アプリの権限、Activity、intent filter、Deep Link、FileProviderなどが定義されます。Flutterプラグインを追加したときに、カメラや位置情報などの権限をManifestへ追加する必要がある場合があります。Manifest設定が不足していると、Dartコードが正しくても機能が動かないことがあります。逆に不要な権限を残すと、ユーザーの信頼やストア審査に影響します。Flutterアプリでも、AndroidManifestはセキュリティと機能の両方に関わる重要ファイルです。
5.3 Gradle設定はビルドに影響する
android フォルダにはGradle関連ファイルが含まれ、Androidアプリのビルド設定を管理します。minSdk、targetSdk、compileSdk、依存関係、signingConfig、productFlavorsなどが関係します。Flutter側でパッケージを追加しただけでも、Android側のGradle設定が原因でビルドエラーが起きることがあります。特にFlutter SDKやAndroid Gradle Pluginの更新、FirebaseやネイティブSDKの導入時には、Gradle設定の理解が必要です。FlutterアプリのAndroidビルド問題を解決するには、DartだけでなくGradleも読めることが重要です。
5.4 MainActivityはネイティブ連携の入口になる
Flutter Androidアプリでは、MainActivity がFlutterエンジンとAndroid側の入口になります。通常はほとんど編集しませんが、Platform ChannelやネイティブSDK連携、特定のプラグイン設定で触る場合があります。KotlinまたはJavaで書かれ、FlutterActivityを継承する構成が一般的です。MainActivityへ多くの処理を詰め込むと、ネイティブ側が複雑になります。実務では、どうしてもAndroid固有処理が必要な場合だけMainActivityや専用クラスに分け、共通ロジックはDart側に保つのが基本です。
5.5 Android flavorは環境別ビルドに使う
FlutterでもAndroidのproductFlavorsを使って、開発版、ステージング版、本番版、ブランド別版などを作れます。Flutter公式ドキュメントでも、Android向けflavorsを設定し、特定のリリースタイプや開発環境向けのビルドを作成する方法が案内されています。 flavorsを使うと、アプリ名、applicationId、アイコン、API設定、Firebase設定などを切り替えられます。ただし、Flutter側のdart-defineや環境設定とも整合させる必要があります。Android flavorだけを設定しても、Dart側の環境切り替えが整理されていなければ混乱します。
5.6 Android側変更はFlutter側と連携して管理する
Flutterアプリでは、Android側設定とDart側コードが連携して動きます。たとえば、通知機能ではAndroidの権限やManifest、Firebase設定、Dart側の通知処理がすべて関係します。どちらか一方だけを変更すると、不具合が起こる可能性があります。実務では、Android側変更をDart側変更と同じPull Requestで確認し、必要な権限、Gradle、Manifest、設定ファイルが揃っているかを見ることが重要です。Flutterは共通コード開発を効率化しますが、ネイティブ設定との整合性を保つことが安定運用の鍵になります。
6. iosフォルダの構成
ios フォルダは、FlutterアプリのiOS側ネイティブプロジェクトを管理する場所です。Xcode設定、Info.plist、Podfile、Runner Target、署名、Capabilitiesなどが含まれます。
6.1 iosフォルダはXcodeプロジェクトを含む
Flutterプロジェクトの ios フォルダには、Xcodeで開くRunnerプロジェクトやWorkspaceが含まれます。iOS向けビルド、署名、Capabilities、Info.plist、CocoaPods関連設定などはこの中で管理されます。通常のFlutter画面やロジックはDartで書きますが、カメラ、写真、位置情報、通知、Sign in with Apple、Associated Domainsなどを使う場合、iOS側設定が必要になります。Flutter開発者でも、iOSフォルダの基本構成を理解していないと、App Store配布や実機ビルドでつまずきやすくなります。
6.2 Info.plistは権限説明やURL Schemeを管理する
iOS側の Info.plist には、権限説明文、URL Scheme、画面向き、バックグラウンドモード、外部連携設定などが含まれます。カメラ、写真、位置情報、マイクなどを使う場合、ユーザーに表示する利用目的説明が必要です。説明文が不足していると、アプリがクラッシュしたり、審査で問題になったりする可能性があります。Flutterプラグインを追加しただけではInfo.plist設定が自動で完結しない場合もあるため、プラグイン導入時にはiOS側の必要設定を必ず確認する必要があります。
6.3 PodfileはiOS依存関係に関わる
FlutterのiOSビルドでは、CocoaPodsが使われることがあります。ios/Podfile にはiOSネイティブ依存関係の設定が含まれ、FlutterプラグインのiOS側コードがPodsとして組み込まれます。Flutter公式のpackages・plugins開発ドキュメントでも、プラグイン依存関係を取得してリンクするために、アプリの pubspec.yaml にプラグインを追加し flutter pub get を実行すると、ネイティブビルド時にSwift Package Manager記述子やCocoaPods podファイルが解決・接続されると説明されています。 iOSビルドで問題が出たときは、DartだけでなくPodfileやPodsの状態も確認する必要があります。
6.4 Runner TargetはiOSアプリ本体を表す
Flutter iOSアプリでは、Runner TargetがiOSアプリ本体を表します。Bundle Identifier、Signing、Capabilities、Deployment Target、Build SettingsなどはRunner Targetに関係します。アプリをTestFlightやApp Storeへ出す場合、Runner Targetの設定が正しいことが重要です。Flutter側のコードが正常でも、署名設定やBundle Identifierが間違っていると配布できません。iOS側ではXcodeの設定がリリースに直結するため、Flutter開発でもRunner Targetの基本を理解しておく必要があります。
6.5 iOS flavorはSchemeやConfigurationと関係する
Flutter公式ドキュメントでは、iOSやmacOS向けのFlutter flavorは、アプリの特定バージョンをどのようにビルド・実行するかを定義する設定の集合であり、アイコン、アプリ名、APIキー、機能フラグ、ログレベルなどを変えられると説明されています。 iOSでflavorを使う場合、XcodeのScheme、Build Configuration、Bundle Identifier、Info.plist、Firebase設定などを連携させる必要があります。Androidのflavor設定と同じ感覚で進めると、iOS側のScheme管理で混乱することがあります。Flutterの環境別構成では、Dart側、Android側、iOS側を一貫して設計することが大切です。
6.6 iOS側設定は実機と配布で必ず確認する
iOS側設定は、シミュレーターでは動いても実機やTestFlightで問題が出ることがあります。署名、Push通知、Keychain、Associated Domains、In-App Purchase、カメラ、写真、位置情報などは実機確認が重要です。Flutterアプリでも、iOS固有機能はXcodeやApple Developer設定と密接に関係します。実務では、iOS設定変更を行ったら、シミュレーターだけでなく実機、TestFlight、必要なら本番相当環境で確認するべきです。Flutterの共通コードだけで安心せず、iOS固有の品質確認も構成に組み込む必要があります。
7. assetsフォルダと画像管理
Flutterアプリでは、画像、JSON、アニメーション、設定ファイルなどのassetsを管理することが多くあります。assetsは pubspec.yaml と連携して読み込まれるため、フォルダ構成と登録方法を正しく理解する必要があります。
7.1 assetsはpubspec.yamlで登録する
Flutter公式ドキュメントでは、Flutterはプロジェクトルートの pubspec.yaml を使って、アプリに必要なassetsを識別すると説明されています。 つまり、assets/images/logo.png のようにファイルを置くだけでは不十分で、pubspecに登録して初めてアプリから読み込めます。登録漏れがあると、実行時に画像が表示されなかったり、アセット読み込みエラーが出たりします。Flutterではassetsとpubspecが密接に関係しているため、画像を追加するたびにpubspecの記述とインデントを確認する必要があります。
7.2 imagesとiconsを分ける
実務では、assets/images と assets/icons を分けて管理することが多いです。画像はオンボーディング、バナー、背景、イラストなどに使われ、アイコンはボタン、タブ、メニュー、状態表示などに使われます。用途が違うリソースを同じフォルダに置くと、アプリが大きくなるほど探しにくくなります。assets/images/onboarding、assets/images/backgrounds、assets/icons/navigation のようにさらに分類すると、デザイン変更時にも修正対象を見つけやすくなります。assets管理はUI開発の効率に直結します。
7.3 解像度別画像を管理する
Flutterでは、解像度別の画像を用意し、端末の画面密度に応じて適切な画像を選ぶことができます。Flutter公式ドキュメントでは、assetsのパスを pubspec.yaml に指定すると、隣接するサブディレクトリの同名ファイルもasset bundleに含まれ、Flutterが解像度に適した画像を選ぶと説明されています。 画像の品質やサイズはアプリ体験に影響するため、必要に応じて1.5x、2.0x、3.0xなどの画像を管理します。ただし、高解像度画像を無計画に増やすとアプリサイズが大きくなるため、用途に応じた最適化も重要です。
7.4 JSONやLottieもassetsで管理する
Flutterアプリでは、画像だけでなく、JSONファイル、Lottieアニメーション、初期データ、設定ファイルなどもassetsとして管理することがあります。これらは assets/json、assets/lottie、assets/config のように用途別に分けると分かりやすくなります。JSONをアプリ内に同梱する場合、更新頻度やサイズも考慮する必要があります。頻繁に変更されるデータをassetsに入れると、アプリ更新なしに変更できません。assetsに入れるべきものと、サーバーから取得すべきものを区別することが重要です。
7.5 assets名は用途が分かるようにする
assetsの命名は、後から探しやすいように用途が分かる名前にするべきです。logo.png、bg_login_header.png、ic_home.svg、illustration_empty_cart.png のように、画面や用途を含めると管理しやすくなります。image1.png や new_icon.png のような名前は、時間が経つと意味が分からなくなります。デザイナーと開発者で命名規則を共有し、Pull Requestでassets追加を確認する運用が望ましいです。assets名はUI資産を探すための重要な検索キーです。
7.6 不要assetsは定期的に削除する
アプリ開発が長く続くと、使われなくなった画像や古いアニメーション、仮素材が残りがちです。不要assetsはアプリサイズを増やし、管理を複雑にします。Flutterではassetsをpubspecでまとめて登録することが多いため、フォルダごと登録している場合、使っていないファイルまでasset bundleに含まれる可能性があります。定期的に未使用assetsを確認し、不要なものを削除することが重要です。assetsは追加するだけでなく、整理し続けることでアプリ品質とサイズを保てます。
8. fontsとテーマ構成
Flutterでは、カスタムフォントやテーマを使ってアプリ全体の見た目を統一できます。フォント、色、文字スタイル、余白、ボタンなどをDesign Systemとして管理すると、長期的に保守しやすくなります。
8.1 フォントはpubspec.yamlで定義する
Flutter公式のフォント利用ドキュメントでは、フォントファイルを追加し、pubspec.yaml にフォント定義を記述して、weightやstyleごとのフォントファイルを指定する方法が説明されています。 フォント設定はインデントやファイルパスを間違えると反映されません。特に複数weightやitalicを使う場合、ファイルと指定値の対応が正しいか確認する必要があります。フォントはアプリ全体の印象を左右するため、デザイン担当者と連携しながら管理することが重要です。
8.2 ThemeDataで全体デザインを管理する
Flutterでは、ThemeData を使ってアプリ全体の色、文字、ボタン、AppBar、InputDecorationなどの見た目を管理できます。画面ごとに直接色やフォントを指定すると、デザイン変更時に修正箇所が増えます。ThemeDataへ共通設定をまとめれば、アプリ全体の一貫性を保ちやすくなります。実務では、app_theme.dart や theme.dart のようなファイルを作り、light themeとdark themeを分けることが多いです。テーマ管理は、FlutterアプリのUI品質を支える重要な構成です。
8.3 TextThemeで文字スタイルを統一する
見出し、本文、注釈、ボタン、ラベルなどの文字スタイルは、TextTheme で統一すると便利です。各画面で個別にfontSizeやfontWeightを指定すると、画面ごとの差分が増え、デザインが崩れやすくなります。TextThemeを使えば、デザインシステムに沿った文字スタイルを共通利用できます。さらに、将来的にフォントサイズや行間を変更する場合も、一箇所の修正で全体へ反映しやすくなります。文字スタイルはUIの可読性に直結するため、プロジェクト構成上も明確に管理すべきです。
8.4 ColorSchemeで意味ベースの色を使う
Flutterでは、ColorSchemeを使ってprimary、secondary、surface、errorなどの意味ベースの色を管理できます。色を blue や red のような見た目だけで扱うと、ブランド変更やダークモード対応で意味がずれやすくなります。意味ベースで色を管理すれば、画面ごとに適切な役割の色を使いやすくなります。たとえば、エラー表示にはerror、背景にはsurface、主要ボタンにはprimaryを使うようにします。ColorSchemeを活用すると、Material Designとの整合性も取りやすくなります。
8.5 ダークモード対応を構成に入れる
ダークモードに対応する場合、light themeとdark themeを明確に分けて管理する必要があります。各画面で直接色を指定していると、ダークモードで文字が見えにくい、背景とのコントラストが低いといった問題が起こります。ThemeDataとColorSchemeを使って全体管理すれば、ダークモード対応がしやすくなります。実務では、画面ごとに見た目を確認し、画像やassetsもダークモードに合うかを確認する必要があります。テーマ構成は、アクセシビリティやユーザー体験にも関わります。
8.6 Design Systemとして独立させる
大規模Flutterアプリでは、テーマ、色、文字、ボタン、入力欄、カード、余白、アイコンをDesign Systemとして独立させることがあります。lib/core/design_system や packages/design_system のように分けると、複数機能で同じUIルールを使いやすくなります。Design Systemを独立させると、画面実装者が自由にデザイン値を決めるのではなく、共通ルールに沿ってUIを作れます。ただし、Design Systemにも運用が必要です。部品追加の基準、命名、レビュー、デザイナーとの連携を決めることで、長期的に品質を保てます。
9. 状態管理の構成
Flutterアプリでは、状態管理の設計がプロジェクト構成に大きく影響します。画面状態、アプリ全体状態、非同期処理、キャッシュ、フォーム入力などをどこで管理するかを明確にする必要があります。
9.1 状態管理は画面肥大化を防ぐ
FlutterではWidget内に状態を書くこともできますが、画面が複雑になるとWidgetが肥大化します。ローディング、エラー、成功、入力値、リスト更新、API通信結果をすべてStatefulWidget内で扱うと、UIとロジックが混ざって読みにくくなります。状態管理をViewModel、Controller、Notifierなどへ分けることで、Widgetは表示に集中できます。状態管理の構成が整理されていると、UI変更とロジック変更の影響を分離でき、テストもしやすくなります。
9.2 ProviderやRiverpodを使う構成
Flutterでは、ProviderやRiverpodのような状態管理ライブラリがよく使われます。これらを使う場合、Provider定義、Notifier、Repository、UseCaseの配置ルールを決めることが重要です。画面ファイルの中にProvider定義を直接書きすぎると、再利用やテストが難しくなります。実務では、featureごとにproviderファイルを分けたり、domainやdata層と依存関係を整理したりします。状態管理ライブラリは便利ですが、構成ルールがなければプロジェクト全体が複雑になります。
9.3 BlocやCubitを使う構成
BlocやCubitを使う場合、Event、State、BlocまたはCubitをどこに置くかを決める必要があります。機能別構成では、features/login/bloc のように画面機能ごとにまとめることが多いです。Blocは状態遷移を明確に表現できるため、大規模アプリや複雑な画面に向いています。一方で、ファイル数が増えやすく、小規模アプリでは過剰に感じる場合もあります。状態管理手法は、チームの理解度、アプリ規模、テスト方針に合わせて選ぶべきです。
9.4 ローカル状態とグローバル状態を分ける
すべての状態をグローバルに管理する必要はありません。テキストフィールドの一時入力、チェックボックスの開閉、アニメーション状態などは、画面内のローカル状態で十分な場合があります。一方、ログイン状態、ユーザー情報、テーマ設定、言語設定、カート情報などは、アプリ全体で共有する状態として管理することがあります。ローカル状態とグローバル状態の区別が曖昧だと、不要に依存が広がります。状態の寿命と利用範囲を考えて配置することが重要です。
9.5 非同期状態を明確に表現する
API通信やデータ読み込みでは、ローディング、成功、空データ、エラーといった状態が発生します。これらを曖昧なboolやnullableな値だけで表すと、UI側の条件分岐が複雑になります。実務では、sealed class、freezed、AsyncValueなどを使って非同期状態を明確に表現することがあります。状態が明確であれば、画面はそれぞれの状態に応じたUIを表示するだけで済みます。状態管理の構成は、ユーザー体験の安定性にも関わります。
9.6 状態管理はテスト可能にする
状態管理のコードは、Unit Testの対象にしやすい形にしておくべきです。ViewModel、Notifier、Bloc、CubitがRepositoryやUseCaseを外から受け取る構成にすると、テスト時にFakeへ差し替えられます。状態遷移をテストできれば、UIを起動しなくても多くのロジックを確認できます。逆に、Widget内にロジックを詰め込むとWidget Testに頼る必要が増え、テストが重くなります。状態管理を分離することは、保守性だけでなくテスト戦略にも大きく影響します。
10. ルーティングとNavigation構成
Flutterアプリでは、画面遷移をどこで管理するかが重要です。Navigator、Router、GoRouter、auto_routeなどを使う場合でも、ルーティング構成を整理しておくことで保守性が高まります。
10.1 ルーティングはアプリ全体の地図になる
ルーティングは、ユーザーがどの画面からどの画面へ移動できるかを定義するアプリ全体の地図です。小規模アプリでは、画面内で Navigator.push を直接呼んでも問題ありませんが、大規模アプリでは画面遷移が散らばり、全体像が見えにくくなります。実務では、router.dart、app_router.dart、routes.dart のような専用ファイルを用意し、画面パスや遷移定義を一元管理することが多いです。ルーティングを整理すると、Deep Linkや認証ガードも扱いやすくなります。
10.2 Navigator 1.0はシンプルな構成に向いている
従来のNavigator APIは、シンプルな画面遷移に向いています。Navigator.push や Navigator.pop を使えば、画面を積み重ねる形で遷移できます。小規模アプリや画面数が少ないアプリでは、この方法でも十分に開発できます。ただし、Deep Link、Web対応、複雑な認証状態、タブごとのNavigation Stackが必要になると、画面ごとの直接遷移だけでは管理が難しくなります。アプリ規模が大きくなったら、ルーティングを専用構成へ移すことを検討するべきです。
10.3 GoRouterは宣言的ルーティングに使いやすい
GoRouterは、Flutterでよく使われる宣言的なルーティング手法の一つです。ルートパス、画面、リダイレクト、ShellRouteなどを定義し、Deep LinkやWeb URLとも連携しやすい構成を作れます。GoRouterを使う場合、ルート定義を画面ファイル内に散らばらせず、専用ファイルにまとめると保守しやすくなります。認証状態に応じてログイン画面へリダイレクトする処理も、ルーティング層で扱うと整理しやすいです。ルーティングはアプリ全体の構造なので、状態管理とも連携して設計する必要があります。
10.4 タブ構成はNavigation Stackを分ける
BottomNavigationBarやNavigationBarを使うアプリでは、タブごとに独立したNavigation Stackを持つことがあります。たとえば、Homeタブで詳細画面へ進み、Searchタブへ移動して戻ってもHomeタブの状態を保持したい場合があります。このような構成では、タブごとのNavigatorを分けたり、ShellRouteを使ったりします。タブ構成を単純に画面切り替えだけで実装すると、戻る動作や状態保持で問題が起こることがあります。タブ構成はユーザー体験に直結するため、早い段階で設計しておくことが重要です。
10.5 Deep Linkはルーティングと統合する
FlutterアプリでDeep LinkやUniversal Links、App Linksを使う場合、外部URLから特定画面へ遷移する必要があります。これはDart側のルーティングだけでなく、Android ManifestやiOS Associated Domains、Info.plistなどのネイティブ設定とも関係します。Deep Linkを扱う場合、URL解析、認証状態、存在しないリソース、パラメータ不足への対応を考える必要があります。ルーティング構成が整理されていれば、Deep Linkも一元的に管理しやすくなります。外部入力を扱うため、セキュリティ面の確認も重要です。
10.6 ルーティングはテストしやすくする
ルーティングの判断を画面内に散らばらせると、テストが難しくなります。認証済みならホームへ、未認証ならログインへ、権限不足ならエラー画面へ、という判断はルーティング層やNavigation Controllerでテストできるようにするべきです。ルート名やパスを定数化し、画面引数の型を明確にすると、遷移ミスも減らせます。FlutterではWidget Testで画面遷移を確認することもできますが、すべてをUIテストに頼るより、ルーティングロジックを分離したほうが保守しやすくなります。
11. data層とRepository構成
FlutterアプリがAPIやローカル保存を扱う場合、data層とRepositoryの設計が重要になります。画面から直接APIを呼ぶ構成は簡単ですが、アプリが大きくなるほど保守しにくくなります。
11.1 data層は外部データとの境界になる
data層は、API、ローカルDB、SharedPreferences、Secure Storage、ファイル、外部SDKなど、アプリ外部または永続化されたデータとのやり取りを管理する場所です。画面やViewModelが直接HTTPクライアントを呼ぶと、API仕様変更や保存方式変更の影響がUIまで広がります。data層を分けることで、外部データ形式をアプリ内部のModelへ変換し、UI側を外部仕様から守れます。Flutterアプリでも、データ取得と表示を分けることは長期保守に非常に重要です。
11.2 Repositoryはデータ取得の窓口になる
Repositoryは、API、キャッシュ、ローカル保存などをまとめる窓口です。ViewModelやUseCaseは、APIやDBの詳細を知らずにRepositoryを呼び出します。たとえば、ユーザー情報を取得する処理では、まずキャッシュを見て、なければAPIを呼び、結果を保存するという処理をRepository内に隠せます。これにより、データ取得方法が変わっても呼び出し側への影響を小さくできます。Repositoryは、Flutterアプリのデータ層を整理するうえで非常に重要な役割を持ちます。
11.3 DataSourceで取得元を分ける
Repositoryの内部では、RemoteDataSourceとLocalDataSourceを分けることがあります。RemoteDataSourceはAPI通信、LocalDataSourceはDBやローカルストレージを担当します。この分け方をすると、どこからデータを取得するかの責務が明確になります。APIレスポンスが変わった場合はRemoteDataSourceやMapperを修正し、保存方式が変わった場合はLocalDataSourceを修正すればよくなります。DataSourceを分けることで、Repositoryが巨大化するのも防ぎやすくなります。
11.4 DTOとDomain Modelを分ける
APIレスポンス用のDTOと、アプリ内部で使うDomain Modelを分けると保守しやすくなります。APIのフィールド名やnull許容、日付形式、ネスト構造は外部仕様に依存します。これをそのままUIまで持ち込むと、API変更の影響が広がります。DTOをMapperでDomain Modelへ変換すれば、UIやUseCaseは安定した内部表現を使えます。小規模アプリでは少し手間に見えますが、APIが増えるほどこの分離の価値は大きくなります。
11.5 エラー処理をdata層で整理する
API通信では、ネットワークエラー、認証エラー、タイムアウト、サーバーエラー、パースエラーなどが発生します。これらを画面ごとに直接処理すると、同じようなエラーハンドリングが重複します。data層で例外やResult型を整理し、上位層へ分かりやすい形で返すと、UI側は表示に集中できます。たとえば、通信失敗を NetworkFailure、認証切れを UnauthorizedFailure のように表現すれば、ViewModelは状態へ変換しやすくなります。エラー設計はユーザー体験にも関わる重要な構成です。
11.6 data層はテストしやすくする
data層をテストしやすくするには、HTTPクライアントやローカル保存を抽象化し、テスト時にFakeへ差し替えられる構成にする必要があります。Repositoryが具体的な通信実装に密結合していると、Unit Testで外部サービスに依存してしまいます。DataSourceやClientをインターフェース化し、Repositoryに注入する構成にすると、テストが安定します。FlutterアプリではDartだけで多くのロジックをテストできるため、data層を整理すれば品質確認がかなりしやすくなります。
12. domain層とUseCase構成
domain層は、アプリ固有の業務ルールや操作を管理する場所です。Clean Architectureを意識する場合、domain層はUIや外部データ仕様から独立した中心的な層になります。
12.1 domain層はアプリ固有のルールを表す
domain層には、アプリが本質的に行う処理やルールを置きます。たとえば、ログイン条件、注文計算、プロフィール更新、通知設定の判定、カート合計計算などが該当します。これらをWidgetやRepositoryに直接書くと、UI変更やAPI変更の影響を受けやすくなります。domain層を分けることで、アプリ固有のルールを安定した場所に保てます。FlutterではUIをDartで書けるため、ついUIにロジックを書きがちですが、長期保守ではdomain層の分離が重要になります。
12.2 UseCaseは操作単位を表す
UseCaseは、ユーザーや画面から見た操作単位を表します。ログインする、商品一覧を取得する、注文を確定する、ユーザー設定を保存する、検索する、という処理をUseCaseとして切り出すと、ViewModelが肥大化しにくくなります。UseCaseはRepositoryを呼び出し、必要な業務ルールを適用し、結果を返します。小規模アプリではすべてをUseCase化する必要はありませんが、複雑な処理や複数画面で使う処理はUseCaseに分ける価値があります。UseCaseは画面とdata層の間を整理する役割を持ちます。
12.3 Entityは外部仕様から独立させる
domain層のEntityは、APIレスポンスやDB構造ではなく、アプリ内部で意味を持つデータ表現です。たとえば、APIでは first_name と last_name が別々でも、アプリ内部では UserName として扱うほうが自然な場合があります。Entityを外部仕様から独立させることで、API変更の影響を小さくできます。data層でDTOをEntityへ変換し、domainやpresentationはEntityを使う構成が保守しやすいです。Entity設計は、アプリの業務理解にも関わります。
12.4 domain層はFlutter非依存に近づける
domain層は、できるだけFlutterフレームワークに依存しない純粋なDartコードに近づけるのが理想です。Widget、BuildContext、Theme、Navigatorなどに依存しないようにすれば、Unit Testが簡単に書けます。また、将来的にCLI、別プラットフォーム、別UIへロジックを再利用する可能性がある場合も有利です。もちろん実務では完全な独立が難しいこともありますが、domain層にUI依存を持ち込まない意識は重要です。domain層はアプリの中核として安定させるべき場所です。
12.5 Repositoryインターフェースをdomainに置く場合がある
Clean Architectureでは、Repositoryのインターフェースをdomain層に置き、実装をdata層に置く構成がよく使われます。これにより、domain層は具体的なAPIやDB実装を知らず、抽象的なデータ取得契約だけを扱います。UseCaseはRepositoryインターフェースに依存し、テストではFake Repositoryを差し替えられます。この構成は少しファイル数が増えますが、依存方向が明確になり、大規模アプリで保守しやすくなります。依存関係の逆転を使うことで、アプリの中核を外部実装から守れます。
12.6 domain層は過剰設計に注意する
domain層は強力ですが、小規模アプリで最初から細かく分けすぎると、開発速度が落ちることがあります。画面数が少なく、業務ルールも単純なアプリでは、ViewModelとRepositoryだけで十分な場合もあります。重要なのは、UseCaseやEntityを導入する理由があるかどうかです。複雑な処理が増えた、複数画面で同じロジックを使う、テストしたい業務ルールがある、という段階でdomain層を強化すると現実的です。設計は目的に合わせて導入するべきです。
13. presentation層とUI構成
presentation層は、ユーザーに見える画面やUI状態を管理する場所です。FlutterではWidgetが中心になりますが、画面、共通部品、ViewModel、UI Stateを分けることで保守性が高まります。
13.1 presentation層は表示責務を持つ
presentation層には、画面Widget、共通Widget、ViewModel、Controller、UI Stateなどを置きます。ここはユーザーに見える部分を担当するため、変更頻度が高い層です。API通信やDB処理を直接ここに書くと、UI変更とデータ処理が密結合になります。presentation層は、domainやdataから受け取った情報をユーザーに分かりやすく表示し、ユーザー操作を上位の処理へ渡す役割に集中させるべきです。表示責務を明確にすることで、画面ファイルが読みやすくなります。
13.2 pagesとwidgetsを分ける
Flutterでは、画面全体を表すWidgetを pages や screens に置き、再利用可能な部品を widgets に置く構成がよく使われます。たとえば、LoginPage、HomePage、ProfilePageはpagesに置き、PrimaryButton、AppTextField、LoadingIndicatorはwidgetsに置きます。この分け方により、画面固有の構成と共通部品を区別できます。pagesにすべてを書き込むとファイルが肥大化し、widgetsを過剰に共通化すると柔軟性が下がります。画面固有部品と共通部品の境界を意識することが重要です。
13.3 UI Stateを明確に定義する
presentation層では、画面がどのような状態を持つかを明確に定義することが重要です。ローディング中、データあり、空データ、エラー、入力中、送信中など、画面状態を表すUI Stateを作ると、Widget側の条件分岐が整理されます。状態を複数のboolで管理すると、矛盾した状態が生まれることがあります。たとえば、loadingがtrueでerrorもある、dataもnullという状態が発生しやすくなります。UI Stateを1つの型として整理すると、画面の振る舞いが安定します。
13.4 ViewModelやControllerを分ける
画面ごとの状態管理やイベント処理は、ViewModelやControllerに分けると保守しやすくなります。WidgetはViewModelから状態を受け取り、ユーザー操作をViewModelへ渡します。ViewModelはUseCaseやRepositoryを呼び出し、結果をUI Stateへ変換します。この構成にすると、Widgetは表示に集中でき、ViewModelはUnit Testの対象にしやすくなります。FlutterではWidgetに直接ロジックを書きやすいため、画面が複雑になる前にViewModel分離を意識することが重要です。
13.5 共通WidgetはDesign Systemと連携する
共通Widgetは、アプリ全体のUI一貫性を保つために重要です。ボタン、入力欄、カード、ダイアログ、エラー表示、空状態表示などを共通化すると、画面ごとの差分を減らせます。ただし、共通Widgetが特定画面の仕様に依存しすぎると再利用しにくくなります。Design Systemと連携し、色、文字、余白、角丸などを共通ルールに沿って設計すると、UI品質が安定します。共通Widgetは、見た目の統一だけでなく開発効率にも大きく影響します。
13.6 presentation層はWidget Testしやすくする
Flutterの強みの一つは、Widget TestでUI部品や画面の表示を比較的高速に確認できることです。Flutter公式ドキュメントでは、Widget TestはWidgetを構築し、Finderで検索し、Matcherで検証する流れが紹介されています。 presentation層をテストしやすくするには、外部依存を直接持たせず、状態や依存を外から渡せる構成にすることが重要です。WidgetがAPI通信を直接行うとテストが重くなります。UIとロジックを分けることで、Widget Testの安定性が高まります。
14. packagesとmonorepo構成
Flutterでは、アプリ内の一部機能をpackageとして切り出すことができます。大規模アプリや複数アプリ開発では、packagesやmonorepo構成が有効になる場合があります。
14.1 packageは再利用可能なDartコードをまとめる
Flutter公式ドキュメントでは、Dart packageは最低限 pubspec.yaml を含むディレクトリであり、依存関係、Dartライブラリ、アプリ、リソース、テスト、画像、フォント、examplesなどを含められると説明されています。 Flutterアプリでは、共通ロジック、Design System、APIクライアント、認証基盤などをpackageとして切り出すことがあります。package化すると、責務が明確になり、別アプリでも再利用しやすくなります。ただし、公開APIや依存関係を設計する必要があるため、導入には一定の設計コストがあります。
14.2 packagesフォルダで内部packageを管理する
monorepo構成では、ルートに packages フォルダを作り、その中に design_system、core_network、auth_client、shared_domain のような内部packageを置くことがあります。アプリ本体はこれらのpackageをpath dependencyとして参照します。この構成により、アプリ本体の lib が肥大化しにくくなり、共通部分を独立してテストできます。複数アプリで同じ基盤を使う場合にも便利です。ただし、package間の依存方向を管理しないと、循環依存や過剰な結合が起きる可能性があります。
14.3 Design Systemをpackage化する
大規模Flutterアプリでは、Design Systemを独立packageにすることがあります。色、テーマ、文字、アイコン、ボタン、入力欄、カード、ダイアログなどをまとめ、複数機能や複数アプリから利用できるようにします。Design System packageは、UI品質を統一し、画面実装者が毎回独自部品を作るのを防ぎます。ただし、Design Systemにビジネスロジックを入れてはいけません。特定画面の都合を共通部品に入れすぎると、packageの再利用性が下がります。見た目と共通UIに責務を限定することが重要です。
14.4 domainやdataをpackage化する
アプリの業務ロジックやデータ層をpackage化することもあります。たとえば、Domain packageにEntityやUseCase、Repository Interfaceを置き、Data packageにRepository実装やAPI Clientを置く構成です。これにより、UI層から業務ロジックやデータ処理を分離でき、テストもしやすくなります。ただし、package化するとファイル数や設定が増えるため、小規模アプリでは過剰になることもあります。複数チームや複数アプリで共有する必要がある場合、package化の価値が大きくなります。
14.5 monorepoはツール運用も必要になる
monorepo構成では、複数packageの依存関係、テスト、バージョン、Lint、コード生成をまとめて管理する必要があります。単純にpackagesフォルダを作るだけでは不十分です。各packageで flutter test や dart analyze を実行できるようにし、CIでもまとめて確認する仕組みが必要です。package間の変更影響を追いやすくするために、依存関係の方向や公開APIを明確にすることも重要です。monorepoは強力ですが、運用ルールがなければ複雑さが増えます。
14.6 package化は段階的に進める
最初からすべてをpackage化する必要はありません。まずはアプリ内の lib 構成を整理し、重複が増えた共通UIや、独立性の高い基盤からpackage化するのが現実的です。早すぎるpackage化は開発速度を下げる場合があります。一方で、肥大化しすぎた後に分離するのも大変です。アプリ規模、チーム人数、再利用性、ビルド時間、テスト容易性を見ながら段階的に進めることが重要です。package化は目的ではなく、保守性を高めるための手段です。
15. pluginsとPlatform Channel構成
Flutterでは、多くのネイティブ機能を既存pluginで利用できますが、独自のネイティブ連携が必要な場合はPlatform Channelやplugin開発を行うことがあります。
15.1 pluginはDartとネイティブ実装をつなぐ
Flutter公式ドキュメントでは、plugin packageはDart APIと、Android、iOS、Web、macOS、Windows、Linuxなどのプラットフォーム固有実装を組み合わせるものとして説明されています。 つまり、Flutter pluginはDart側から使いやすいAPIを提供し、内部で必要に応じてネイティブコードを呼び出します。カメラ、位置情報、通知、Bluetooth、決済、認証など、多くの機能はpluginを通じて利用されます。pluginを導入するときは、Dart側だけでなくAndroidやiOSの追加設定も確認する必要があります。
15.2 Platform Channelは独自連携に使う
Flutter公式ドキュメントでは、カスタムのプラットフォーム固有コードを書く方法としてPlatform Channelが案内されており、Flutter側クライアント、Android実装、iOS実装を追加する流れが説明されています。 Platform Channelを使えば、DartからKotlin、Java、Swift、Objective-Cのネイティブ処理を呼び出せます。既存pluginで対応できない社内SDK、特殊デバイス、OS固有APIを扱う場合に使われます。ただし、Dartとネイティブの境界が増えるため、型、エラー、非同期処理を丁寧に設計する必要があります。
15.3 MethodChannelは呼び出し型の連携に使う
MethodChannelは、Dart側からネイティブ側のメソッドを呼び出す形で連携する仕組みです。たとえば、端末固有情報を取得する、ネイティブSDKの処理を実行する、OS設定を開くといった用途に使えます。MethodChannelでは、メソッド名や引数を文字列やMapで渡すことが多いため、型安全性が弱くなりがちです。実務では、Dart側にラッパークラスを作り、ネイティブとのやり取りを一箇所へ閉じ込めると保守しやすくなります。直接画面からMethodChannelを呼ぶ構成は避けるべきです。
15.4 EventChannelはストリーム型の連携に使う
EventChannelは、ネイティブ側から継続的にイベントをDart側へ流す場合に使います。センサー値、位置情報、接続状態、ネイティブSDKのイベント通知などが例です。MethodChannelが単発の呼び出しに向いているのに対し、EventChannelはストリーム型のデータに向いています。イベントの購読開始と終了、メモリ管理、エラー通知を適切に扱わないと、リークや重複イベントが発生する可能性があります。EventChannelを使う場合は、Dart側のStream管理も含めて構成を設計する必要があります。
15.5 pluginコードは責務を分ける
独自pluginを作る場合、Dart API、Android実装、iOS実装、example、testを整理する必要があります。Dart側には使いやすい抽象APIを置き、ネイティブ側にはプラットフォーム固有処理を閉じ込めます。exampleアプリを用意すると、pluginの動作確認や利用方法の説明に役立ちます。pluginはアプリ本体より再利用性が求められるため、API設計とエラーハンドリングを丁寧に行うべきです。社内専用pluginでも、将来的な再利用や保守を考えて構成することが重要です。
15.6 ネイティブ連携はテストとドキュメントが重要
Platform Channelやpluginは、Dartとネイティブの境界をまたぐため、問題が起きたときの切り分けが難しくなります。どのメソッドが何を受け取り、何を返し、どのエラーが発生するかをドキュメント化しておくと保守しやすくなります。また、Dart側のラッパーはUnit Testで確認し、ネイティブ実装は実機やIntegration Testで確認する必要があります。ネイティブ連携は強力ですが、構成と運用を誤ると複雑さが一気に増えます。
16. testとWidget Test構成
Flutterのテスト構成では、Unit Test、Widget Test、Integration Testを役割に応じて使い分けることが重要です。test フォルダを正しく整理すると、品質保証が継続しやすくなります。
16.1 testフォルダは高速テストの中心になる
Flutterプロジェクトの test フォルダには、Unit TestやWidget Testを配置します。Flutter公式のUnit Testドキュメントでは、Unit Testは単一の関数、メソッド、クラスの動作を検証するのに便利であり、test パッケージや flutter_test が使われると説明されています。 test フォルダに置くテストは比較的高速に実行できるため、CIで毎回実行しやすいです。アプリの基礎品質を守るには、まずUnit TestとWidget Testを充実させることが重要です。
16.2 Unit Testはロジック確認に向いている
Unit Testでは、UseCase、Repository、Formatter、Validator、Mapper、状態管理クラスなどを確認します。API通信やUI表示に依存しない処理ほど、Unit Testで安定して検証できます。たとえば、金額計算、日付変換、入力バリデーション、DTOからEntityへの変換などはUnit Testに向いています。ロジックをWidget内に書かず、domain層やViewModelへ分けておけば、Unit Testが書きやすくなります。テストしやすい構成は、結果的に保守しやすい構成でもあります。
16.3 Widget TestはUI部品を確認する
Widget Testでは、Widgetをテスト環境で構築し、表示や操作を確認できます。Flutter公式ドキュメントでは、flutter_test を使い、WidgetTesterでWidgetをbuildし、FinderでWidgetを探し、Matcherで検証する流れが紹介されています。 Widget Testは、実機を使わずにUIの一部を確認できるため、画面部品や状態表示の検証に向いています。たとえば、ローディング時にProgressIndicatorが出るか、エラー時にメッセージが出るかを確認できます。Widget Testを活用すると、UI変更の安全性が高まります。
16.4 Golden Testで見た目を確認する
Flutterでは、Golden Testを使ってWidgetの見た目を画像として比較することがあります。Design Systemや共通Widgetの見た目が意図せず変わっていないか確認するのに役立ちます。ただし、フォント、OS、レンダリング差分によって結果が変わる場合があるため、CI環境を安定させる必要があります。Golden Testはすべての画面に導入するより、共通コンポーネントや重要なUI部品に絞ると運用しやすいです。見た目の品質を守るための補助として使うのが現実的です。
16.5 テスト用Fakeを整理する
Unit TestやWidget Testでは、APIやRepositoryをFakeへ差し替えることがよくあります。テスト用Fakeを各テストファイルに散らばらせると、同じようなFakeが重複します。test/helpers、test/fakes、test/mocks のようなフォルダを作り、共通のテスト補助を整理すると保守しやすくなります。ただし、Fakeが本番実装と大きく違いすぎると、テストの信頼性が下がります。テスト用コードもプロジェクト構成の一部として、責務と命名を整理する必要があります。
16.6 testはCIで継続実行する
テストはローカルで書くだけでなく、CIで継続的に実行することが重要です。flutter test をPull Requestごとに実行すれば、ロジックやWidgetの破壊を早期に検出できます。さらに、coverageを取得すれば、どの部分がテストされているかを確認できます。ただし、カバレッジ数値だけを目標にすると意味の薄いテストが増えることがあります。重要なのは、ビジネス上重要なロジックや壊れやすいUIを継続的に守ることです。CIで実行しやすいテスト構成を作ることが、Flutter開発の品質を支えます。
17. integration_test構成
Integration Testは、アプリ全体の動作を実機またはエミュレーターに近い環境で確認するために使います。Unit TestやWidget Testでは確認できない画面遷移や実際の操作フローを検証できます。
17.1 Integration Testはアプリ全体を確認する
Flutter公式ドキュメントでは、Integration Testは完全なアプリの振る舞いを検証するもので、End-to-End TestやGUI Testとも呼ばれると説明されています。Unit TestやWidget Testが個別の関数、クラス、Widgetを確認するのに対し、Integration Testは複数の部品が組み合わさった実際のユーザーフローを確認します。 ログイン、商品購入、検索、プロフィール更新など、重要な操作を実環境に近い形で検証できます。ただし実行時間が長いため、使いどころを選ぶことが重要です。
17.2 integration_testフォルダに配置する
FlutterのIntegration Testでは、integration_test フォルダを使う構成が一般的です。Flutter公式ドキュメントでも、integration_test パッケージを使ってIntegration Testを設定し、テキスト表示の確認、Widgetのタップ、テスト実行を行う流れが紹介されています。 test フォルダとは役割が違うため、Integration Testは専用フォルダに分けると管理しやすくなります。重要なユーザーフローごとにテストファイルを分けると、失敗時の原因も追いやすくなります。
17.3 テスト用環境を分ける
Integration Testでは、実際のAPIや外部サービスに接続するか、テスト用サーバーやMockサーバーを使うかを決める必要があります。本番APIへ直接テストを流すのは危険です。テスト用アカウント、テスト用データ、ステージング環境を用意し、テストが他のユーザーや本番データへ影響しないようにします。Flutterのflavorやdart-defineを使って、Integration Test専用の環境設定を渡すこともあります。Integration Testは強力ですが、環境設計を誤ると不安定で危険なテストになります。
17.4 重要フローに絞って書く
Integration TestはUnit TestやWidget Testより実行時間が長く、端末やエミュレーターの状態にも影響されます。そのため、すべての画面を網羅しようとするとCIが遅くなり、運用が難しくなります。実務では、ログイン、登録、購入、検索、主要フォーム送信など、ビジネス上重要なフローに絞って書くのが現実的です。細かいロジックはUnit Test、UI部品はWidget Test、全体フローはIntegration Testという役割分担を明確にすると、品質と速度のバランスを取りやすくなります。
17.5 CIで実行するタイミングを決める
Integration TestをCIで毎回実行すると、時間がかかる場合があります。Pull Requestごとに短いIntegration Testだけを実行し、重いテストは夜間やリリース前に実行する運用もあります。重要なのは、テストが書かれているだけでなく、適切なタイミングで継続実行されることです。Flutter公式の継続的デリバリードキュメントでも、アプリを頻繁にビルド・配信し、ベータテスターへ届けて検証するためにCI/CDを使う考え方が説明されています。 Integration Testはリリース品質を守るための重要な仕組みです。
17.6 Integration Testは安定性が重要
Integration Testは実環境に近い分、不安定になりやすいです。ネットワーク、アニメーション、タイミング、外部サービス、テストデータの状態によって失敗することがあります。不安定なテストが多いと、チームがテスト結果を信頼しなくなります。安定させるには、テスト用データを固定し、外部依存を減らし、待機処理を適切に使い、テスト間で状態を初期化することが重要です。Integration Testは少数でも信頼性の高いものを維持するほうが、実務では価値があります。
18. flavorsと環境別構成
Flutterアプリでは、開発、ステージング、本番、ブランド別、顧客別などの環境を分けるためにflavorsを使うことがあります。flavorsはDart側、Android側、iOS側を一貫して設計する必要があります。
18.1 flavorはアプリの別バージョンを定義する
Flutter公式ドキュメントでは、Flutter flavorは特定バージョンのアプリをどのようにビルド・実行するかを定義する設定の集合であり、アイコン、アプリ名、APIキー、機能フラグ、ログレベルなどを変えられると説明されています。 たとえば、dev、staging、prodのようなflavorを作ると、同じコードベースから異なる環境向けアプリを作れます。flavorは便利ですが、Dart、Android、iOSの設定が揃っていないと混乱します。環境別構成は、最初に全体設計を決めることが重要です。
18.2 Android flavorとiOS Schemeを対応させる
Flutterでflavorを使う場合、Android側ではproductFlavors、iOS側ではSchemeやBuild Configurationを設定することがあります。Androidだけflavorを設定しても、iOS側で同じ環境が定義されていなければ、プラットフォーム間で挙動がずれます。実務では、dev、staging、prodのような環境名をAndroidとiOSで揃え、Dart側の環境設定とも対応させます。命名がずれると、CIやビルドコマンドが複雑になります。Flutterのflavor設計では、プラットフォーム横断の一貫性が重要です。
18.3 dart-defineでDart側設定を渡す
Flutterでは、--dart-define を使ってDart側へ環境変数のような値を渡すことがあります。API URL、環境名、機能フラグなどをビルド時に渡せます。ただし、dart-defineに入れた値もアプリに含まれる可能性があるため、秘密情報を置くべきではありません。dart-defineは環境切り替えには便利ですが、セキュリティ上の秘密情報保管には向きません。実務では、flavor、dart-define、設定ファイルをどう組み合わせるかを明確にし、誤配布を防ぐ必要があります。
18.4 アプリ名とアイコンを環境別に変える
開発版、ステージング版、本番版を同じ端末にインストールする場合、アプリ名やアイコンを変えると区別しやすくなります。たとえば、開発版は「App Dev」、ステージング版は「App Stg」と表示すれば、テスターが環境を間違えにくくなります。AndroidとiOSでアプリ名やアイコンを変える設定方法は異なるため、それぞれのネイティブ設定を管理する必要があります。見た目の小さな違いですが、誤操作や誤検証を防ぐ実務上の効果は大きいです。
18.5 Firebase設定を環境別に分ける
Firebaseを使う場合、dev、staging、prodで別プロジェクトを使うことがあります。Androidでは google-services.json、iOSでは GoogleService-Info.plist を環境ごとに管理します。これを誤ると、開発版のクラッシュや分析が本番Firebaseへ送られる可能性があります。Flutterアプリでは、Dart側のFirebase初期化だけでなく、AndroidとiOSの設定ファイル配置も確認する必要があります。環境別Firebase設定は、flavor構成の中でも特にミスが起きやすい部分です。
18.6 flavor構成はCI/CDと連携させる
flavorを導入したら、CI/CDでもどのflavorをビルドするかを明確にする必要があります。dev flavorは社内配布、staging flavorはQA検証、prod flavorはストア提出のように、用途ごとのパイプラインを作ると安全です。ビルドコマンド、署名、dart-define、ネイティブ設定、配布先を一貫して管理しないと、誤った成果物を配布する可能性があります。flavorは環境管理を便利にしますが、自動化とドキュメントがなければ運用が複雑になります。CI/CDとセットで設計することが重要です。
19. CI/CDとリリース構成
Flutterプロジェクトでは、CI/CDを使ってビルド、テスト、解析、署名、配布を自動化できます。マルチプラットフォーム開発では、AndroidとiOSの両方を安定してビルドできる構成が重要です。
19.1 CIでflutter analyzeとtestを実行する
FlutterプロジェクトのCIでは、flutter analyze と flutter test を実行することが一般的です。analyzeで静的解析を行い、testでUnit TestやWidget Testを確認できます。これにより、コード品質や基本的な不具合をPull Requestの段階で検出できます。ローカルで動いていても、CIで失敗する場合は環境差分や依存関係の問題が見つかることがあります。CIはチーム全体で同じ品質基準を保つための仕組みです。Flutterプロジェクト構成では、CIで実行しやすいテストと解析の配置が重要になります。
19.2 AndroidとiOSのビルドを分けて管理する
Flutterは共通コードからAndroidとiOSを作れますが、CIではそれぞれのビルド環境が異なります。AndroidはGradleとJDK、iOSはXcodeと署名設定が必要です。Androidビルドだけ通っても、iOSビルドでCocoaPodsや署名エラーが起きることがあります。そのため、CIではAndroidとiOSのビルドを分けて確認することが重要です。特にプラグイン追加時は、片方のプラットフォームだけ壊れることがあります。Flutterのマルチプラットフォーム性を活かすには、両方のビルド確認を自動化する必要があります。
19.3 継続的デリバリーを設計する
Flutter公式の継続的デリバリードキュメントでは、アプリを手作業に頼らず頻繁にビルド・配布し、ベータテスターへ届けて検証するための考え方が説明されています。 Flutterアプリでは、Androidの内部テスト、Firebase App Distribution、iOSのTestFlightなどを使って配布を自動化できます。手作業で毎回ビルドすると、flavor、署名、設定ファイル、dart-defineを間違えるリスクがあります。CI/CDで手順を標準化すると、リリース品質が安定します。
19.4 署名情報を安全に管理する
Androidのkeystore、iOSの証明書やProvisioning Profileは、リリースに必要な機密情報です。これらをリポジトリに直接置くのは危険であり、CIのsecret機能や安全なストレージで管理する必要があります。署名情報が属人化すると、担当者が不在のときにリリースできない可能性があります。CI/CDでは、署名情報を安全に注入し、必要なflavorやbuild modeで成果物を作れる構成にします。署名管理は、セキュリティとリリース安定性の両方に関わる重要な領域です。
19.5 build modesを理解する
Flutterにはdebug、profile、releaseのビルドモードがあります。Flutter公式のテスト・デバッグ関連ページでも、Flutterのbuild modesとしてdebug、release、profileをいつ使うか説明するドキュメントが案内されています。 debugは開発中の確認、profileはパフォーマンス計測、releaseは本番配布に使います。releaseでは最適化が有効になるため、debugでは見えなかった問題が出る可能性もあります。CI/CDでは、目的に応じて適切なbuild modeを使い分けることが重要です。
19.6 リリース前チェックを自動化する
Flutterアプリのリリース前には、テスト、解析、assets確認、翻訳確認、flavor確認、AndroidとiOSのビルド、署名、ストア提出設定を確認する必要があります。これらを手作業だけで行うと、ミスが起きやすくなります。CI/CDでリリース前チェックを自動化し、成果物名や環境名を明確にすると安全です。特に複数flavorを持つプロジェクトでは、どの環境向けの成果物かを間違えない仕組みが必要です。リリース構成は、プロジェクト構成の最終段階として考えるべきです。
20. Flutterプロジェクト構成のまとめ
Flutterプロジェクト構成は、共通Dartコード、Android・iOSネイティブ設定、pubspec、assets、tests、packages、CI/CDが組み合わさったものです。単一コードベースのメリットを活かすには、各要素の役割を正しく理解する必要があります。
20.1 初心者はlibとpubspecから理解する
Flutter初心者が最初に理解すべきなのは、lib と pubspec.yaml です。lib はアプリ本体のDartコードを置く場所であり、main.dart は起動入口です。pubspec.yaml は依存関係、assets、fonts、Flutter SDK設定を管理する中心です。Flutter公式ドキュメントでも、すべてのFlutterプロジェクトにはpubspecがあり、ツールが必要とするメタデータを含むと説明されています。 この2つを理解すれば、Flutterアプリの基本構造が見えてきます。次にandroid、ios、testを理解すると実務に近づきます。
20.2 小規模アプリはシンプルな構成でよい
小規模なFlutterアプリでは、lib/screens、lib/widgets、lib/models、lib/services、lib/utils のようなシンプルな構成で十分な場合があります。最初からClean Architectureやmonorepoを導入すると、ファイル数が増えすぎて開発速度が落ちることがあります。ただし、シンプルであることと無秩序であることは違います。画面にAPI通信を直接書く、assets名を適当に付ける、pubspecを整理しないと、後から保守が難しくなります。小規模でも最低限の責務分離と命名規則は必要です。
20.3 中規模アプリはfeaturesとcoreを意識する
中規模Flutterアプリでは、画面数、API、状態管理、テストが増えてきます。この段階では、features と core を使った構成が有効です。機能ごとに画面、状態管理、Repository、UseCaseをまとめ、共通処理はcoreやsharedへ分けます。状態管理、ルーティング、assets、環境別設定も整理する必要があります。中規模の段階で構成を整えておけば、大規模化したときの負担を減らせます。Flutterアプリは最初は簡単に作れますが、成長に合わせた構成整理が重要です。
20.4 大規模アプリはpackagesやmonorepoを検討する
大規模Flutterアプリでは、packagesやmonorepo構成を検討する価値があります。Design System、Core Network、Domain、Data、Featureなどをpackage化すると、責務が明確になり、テストや再利用もしやすくなります。Flutter公式ドキュメントでも、Dart packageはpubspecを含み、依存関係、Dartライブラリ、テスト、画像、フォントなどを含められると説明されています。 ただし、package化は管理コストも増えるため、必要性を見極めて段階的に進めるべきです。大規模化では構成と運用の両方が重要になります。
20.5 よくある失敗を避ける
Flutterプロジェクトでよくある失敗は、main.dart にすべてを書く、画面Widgetが肥大化する、状態管理が画面ごとにバラバラ、assetsをpubspecに登録し忘れる、androidやios設定を理解せず触る、flavor設定がDart側とネイティブ側でずれる、テストがない、というものです。これらは最初は小さな問題に見えますが、機能追加やリリース時に大きな負担になります。Flutterは開発が速い反面、構成を雑にしても最初は動いてしまいます。だからこそ、早い段階で構成ルールを決めることが重要です。
20.6 理想は変更しやすい構成
理想的なFlutterプロジェクト構成は、複雑な名前や階層をたくさん持つ構成ではなく、変更しやすい構成です。新しい画面を追加しやすく、API変更に対応しやすく、状態管理を追いやすく、assetsを探しやすく、AndroidとiOSの設定を切り分けやすく、テストとCI/CDを回しやすいことが重要です。小規模ではシンプルに、中規模ではfeaturesとcoreを意識し、大規模ではpackagesやmonorepoを検討する流れが現実的です。Flutterプロジェクト構成は、単なるフォルダ整理ではなく、アプリの品質と開発速度を支える設計そのものです。
おわりに
Flutterプロジェクト構成を理解すると、lib、main.dart、pubspec.yaml、android、ios、test、integration_test、assets、packages の役割が明確になります。Flutterは単一コードベースで複数プラットフォームに対応できる強力なフレームワークですが、実務ではDartコードだけでなく、AndroidやiOSのネイティブ設定、依存関係、環境別構成、テスト、CI/CDまで含めて理解する必要があります。構成を理解していれば、問題がDart側にあるのか、Android側にあるのか、iOS側にあるのかを切り分けやすくなります。
良いFlutterプロジェクト構成は、最初から大規模で複雑なものにする必要はありません。小規模では screens、widgets、models、services のようなシンプルな構成で始め、アプリが成長したら features、core、data、domain、presentation のように責務を分けるのが現実的です。さらに大きくなったら、Design SystemやCore機能をpackage化し、monorepo構成やCI/CDを整備することで、長期的に保守しやすい状態を作れます。
Flutter開発では、画面を速く作る力だけでなく、構成を設計する力が長期的な品質を左右します。pubspecを整理し、assetsとfontsを管理し、状態管理とルーティングを明確にし、AndroidとiOSの設定を理解し、テストとCI/CDを組み込むことで、変更に強く、成長しやすいFlutterアプリを作ることができます。
EN
JP
KR