l

o

a

d

i

n

g

.

.

.

短期にFlutterを学習するブログ STEP8:設計・ディレクトリ構成

2026/02/27

Flutter プログラミング 学習

t f B! P L
eyecatch アプリが大きくなり始めたとき、最初に効いてくるのが設計とディレクトリ構成です。 これはflutterに限りませんが、ディレクトリ構成がわかりやすいと、運用コストが高くなりにくくなります。 このSTEPでは「小さく作って、後から壊れにくい」構成をゴールにします。

Feature単位構成という考え方

画面や機能が増えてくると、models や services のような役割分離だけでは破綻しやすくなります。 そこで有効なのが、機能(Feature)ごとにまとめる構成です。 この構成のポイントは、「この機能を消したら、どこを削除すればいいかが一目で分かる」状態を作ることです。 例えば「ユーザー一覧」という機能に関するものは、UIもロジックも同じ場所に置きます。 探し回らなくていい設計は、それだけで保守コストを下げます。
lib/ ├ features/ │ └ user/ │ ├ user_page.dart │ ├ user_view_model.dart │ ├ user_repository.dart │ └ user.dart ├ core/ │ ├ api_client.dart │ └ exception.dart └ main.dart

Clean Architectureを軽量に使うという割り切り

Clean Architectureをそのまま適用すると、個人開発や小規模案件では重くなりがちです。 ここでは「思想だけ借りる」というスタンスを取ります。 意識するのは次の一点だけです。
・UIはロジックを知らない。 ・ロジックはUIを知らない。
UseCaseやEntityを細かく分けすぎず、ViewModelRepositoryの分離ができていれば十分実務に耐えます。 // user_view_model.dart import 'package:flutter/material.dart'; import 'user_repository.dart'; import 'user.dart'; class UserViewModel extends ChangeNotifier { final UserRepository repository; UserViewModel(this.repository); List<User> users = []; bool isLoading = false; Future<void> loadUsers() async { isLoading = true; notifyListeners(); users = await repository.fetchUsers(); isLoading = false; notifyListeners(); } }

チーム開発を前提にした設計の視点

一人で書くコードと、複数人で触るコードは別物です。 チーム開発前提では「迷わない」ことが何より重要になります。
・ファイル名から役割が分かる ・責務が1ファイルに詰め込まれていない ・同じパターンが全Featureで揃っている
この3点を守るだけで、レビューコストは大きく下がります。 設計は未来の自分と他人への説明資料だと考えると、判断しやすくなります。

成果物:実務想定プロジェクト雛形

最後に、このSTEPの完成形としての最小雛形です。 APIからユーザー一覧を取得して表示する、実務でよくある構成です。 lib/main.dart - アプリ起動点 - DI(Provider設定)をここで行う import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'features/user/user_page.dart'; import 'features/user/user_repository.dart'; import 'features/user/user_view_model.dart'; void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider( create: (_) => UserViewModel(UserRepository()), ), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'STEP8 Sample', theme: ThemeData(useMaterial3: true), home: UserPage(), ); } } lib/features/user/user.dart - Entity(純粋なデータ構造) - UI / API / Flutterに依存しない class User { final int id; final String name; final String email; User({ required this.id, required this.name, required this.email, }); factory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'], name: json['name'], email: json['email'], ); } } lib/features/user/user_repository.dart - データ取得責務 - API仕様が変わっても、UIに影響を出さないための層 import 'dart:convert'; import 'package:http/http.dart' as http; import 'user.dart'; class UserRepository { Future<List<User>> fetchUsers() async { final res = await http.get( Uri.parse('https://jsonplaceholder.typicode.com/users'), ); final List list = jsonDecode(res.body); return list.map((e) => User.fromJson(e)).toList(); } } lib/features/user/user_view_model.dart - 状態管理とビジネスロジック - UIから直接APIを触らせないための緩衝材 import 'package:flutter/material.dart'; import 'user.dart'; import 'user_repository.dart'; class UserViewModel extends ChangeNotifier { final UserRepository repository; UserViewModel(this.repository); List<User> users = []; bool isLoading = false; Future<void> loadUsers() async { isLoading = true; notifyListeners(); users = await repository.fetchUsers(); isLoading = false; notifyListeners(); } } lib/features/user/user_page.dart - UI層 - ViewModelの状態だけを見る import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'user_view_model.dart'; class UserPage extends StatelessWidget { @override Widget build(BuildContext context) { final vm = context.watch<UserViewModel>(); return Scaffold( appBar: AppBar(title: const Text('Users')), body: vm.isLoading ? const Center(child: CircularProgressIndicator()) : ListView.builder( itemCount: vm.users.length, itemBuilder: (_, i) { final user = vm.users[i]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ), floatingActionButton: FloatingActionButton( onPressed: vm.loadUsers, child: const Icon(Icons.download), ), ); } } lib/core/api_client.dart - 今回は直接使っていないが - 実務では Repositoryが依存する共通HTTP層 として配置 import 'package:http/http.dart' as http; class ApiClient { Future<http.Response> get(String url) { return http.get(Uri.parse(url)); } } lib/core/exception.dart - エラー表現の共通化用 - 将来的に例外制御を集約する前提の置き場所 class AppException implements Exception { final String message; AppException(this.message); @override String toString() => message; }

実行時にエラーが出る場合の対処法

これまでの成果物でも、runしたときに、エラーが出る場合は、pubspec.yaml ファイルの内容に以下を追加してください。 dependencies: flutter: sdk: flutter provider: ^6.0.5 http: ^1.2.0 dependencies:がすでに登録されている場合は、不足している項目を追記してください。

このステップのまとめ

flkutterも、他のオブジェクト指向言語と同じ様に、デザインパターンを適用できるというのがわかったとおもいます。 この雛形があるだけで、新しい機能を追加するときの「正解の型」がチーム内に共有されます。 STEP8のゴールは、「設計で悩まない状態を先に作ること。」 次のSTEPでは、この構成を前提に実装速度を一気に上げていきます。

人気の投稿

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

ブログ アーカイブ