Skip to content

フォルダ構成


Flutterアーキテクチャとフォルダ構成

Section titled “Flutterアーキテクチャとフォルダ構成”

Flutterプロジェクトのフォルダ構成は、アプリケーションの開発効率とメンテナンス性を向上させるために重要です。以下に、主要なフォルダの役割を紹介します。

なぜアーキテクチャが重要なのか

Section titled “なぜアーキテクチャが重要なのか”

問題のあるコード(アーキテクチャがない場合)

Section titled “問題のあるコード(アーキテクチャがない場合)”

問題のあるコード:

// 問題: すべてのロジックが1つのファイルに混在
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<User> users = [];
@override
void initState() {
super.initState();
// 問題: API呼び出しがWidget内にある
fetchUsers();
}
// 問題: ビジネスロジックがWidget内にある
Future<void> fetchUsers() async {
final response = await http.get(Uri.parse('https://api.example.com/users'));
final data = json.decode(response.body);
setState(() {
users = (data as List).map((json) => User.fromJson(json)).toList();
});
}
// 問題: UIロジックとビジネスロジックが混在
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
// 問題: データ変換ロジックがWidget内にある
subtitle: Text('${users[index].email} (${users[index].age}歳)'),
);
},
),
);
}
}
// 問題点:
// 1. テストが困難(Widget、API、ビジネスロジックが混在)
// 2. 再利用性が低い(他の画面で同じロジックを使えない)
// 3. 責務が不明確(どこを変更すべきか分からない)
// 4. 状態管理が複雑(setStateが散在)

解決: 適切なアーキテクチャ

models/user.dart
// 解決: レイヤーごとに分離
class User {
final int id;
final String name;
final String email;
final int age;
User({required this.id, required this.name, required this.email, required this.age});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
age: json['age'],
);
}
}
// services/user_service.dart
class UserService {
Future<List<User>> getUsers() async {
final response = await http.get(Uri.parse('https://api.example.com/users'));
final data = json.decode(response.body);
return (data as List).map((json) => User.fromJson(json)).toList();
}
}
// providers/user_provider.dart
class UserProvider with ChangeNotifier {
final UserService _userService = UserService();
List<User> _users = [];
bool _isLoading = false;
List<User> get users => _users;
bool get isLoading => _isLoading;
Future<void> fetchUsers() async {
_isLoading = true;
notifyListeners();
_users = await _userService.getUsers();
_isLoading = false;
notifyListeners();
}
}
// screens/home_page.dart
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<UserProvider>(
builder: (context, userProvider, child) {
if (userProvider.isLoading) {
return CircularProgressIndicator();
}
return ListView.builder(
itemCount: userProvider.users.length,
itemBuilder: (context, index) {
final user = userProvider.users[index];
return ListTile(
title: Text(user.name),
subtitle: Text('${user.email} (${user.age}歳)'),
);
},
);
},
);
}
}
// メリット:
// 1. 各レイヤーを独立してテスト可能
// 2. ビジネスロジックの再利用が容易
// 3. 責務が明確(変更箇所が明確)
// 4. 状態管理が一元化される

メインのDartコードを書く場所です。

  • main.dart: アプリのエントリーポイントです。アプリケーションの起動とルーティングを管理します。
  • screens/: 各画面のUIを定義します。画面ごとにファイルを分けて管理します。
  • widgets/: 再利用可能なウィジェットを定義します。共通のUIコンポーネントをここに配置します。
  • models/: データモデルを定義します。アプリケーションで使用するデータ構造をここに配置します。
  • services/: API通信やデータベース処理を行うサービスを定義します。
  • utils/: ユーティリティ関数を定義します。共通のロジックをここに配置します。
  • route/: ルーティング関連のファイルを管理します。
    • routes.dart: アプリケーションのルーティングを管理します。

Android固有の設定やネイティブコードを管理します。

  • app/build.gradle: Android依存関係とビルド設定を管理します。
  • app/src/main/AndroidManifest.xml: アプリの権限、アクティビティ設定を管理します。
  • app/src/main/kotlin/: Android用のネイティブコードを管理します(必要な場合)。
  • app/src/main/res/: アイコン、スプラッシュ画面などのリソースを管理します。

iOS固有の設定やネイティブコードを管理します。

  • Runner.xcodeproj: Xcodeプロジェクト設定を管理します。
  • Runner/Info.plist: iOS権限、設定情報を管理します。
  • Runner/Assets.xcassets: アプリアイコン、画像リソースを管理します。

画像、フォント、設定ファイルなどの静的リソースを管理します。

ユニットテストとウィジェットテストを管理します。

Web版アプリ用の設定を管理します。

フォルダ構成を設計する際のベストプラクティスや、よくある問題の解決策を以下に示します。

  • 一貫した命名規則: フォルダやファイルの命名規則を一貫させ、チーム全体で統一します。
  • モジュール化: 機能ごとにフォルダを分け、コードのモジュール化を図ります。これにより、コードの再利用性とメンテナンス性が向上します。
  • ドキュメントの整備: 各フォルダの役割や使用方法をドキュメント化し、新しいメンバーがプロジェクトに参加しやすくします。
  • フォルダの肥大化: フォルダが肥大化した場合、サブフォルダを作成して整理します。例えば、screens/フォルダ内にhome/settings/などのサブフォルダを作成します。

  • 依存関係の管理: 依存関係が複雑になる場合、pubspec.yamlを適切に管理し、不要な依存関係を削除します。

  • プラットフォーム固有のコードの管理: プラットフォーム固有のコードは、android/ios/フォルダに分けて管理し、共通コードと分離します。

アーキテクチャパターンの選択

Section titled “アーキテクチャパターンの選択”

Provider(推奨):

// シンプルで学習コストが低い
class UserProvider with ChangeNotifier {
List<User> _users = [];
List<User> get users => _users;
Future<void> fetchUsers() async {
_users = await userService.getUsers();
notifyListeners();
}
}
// 使用例
Consumer<UserProvider>(
builder: (context, provider, child) {
return Text('${provider.users.length} users');
},
)

Riverpod(次世代Provider):

// より型安全で、コンパイル時にエラーを検出
final userProvider = FutureProvider<List<User>>((ref) async {
return await userService.getUsers();
});
// 使用例
Consumer(
builder: (context, ref, child) {
final users = ref.watch(userProvider);
return users.when(
data: (users) => Text('${users.length} users'),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
},
)

Bloc(複雑な状態管理):

// イベント駆動の状態管理
class UserEvent {}
class FetchUsers extends UserEvent {}
class UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final List<User> users;
UserLoaded(this.users);
}
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc() : super(UserLoading()) {
on<FetchUsers>((event, emit) async {
emit(UserLoading());
final users = await userService.getUsers();
emit(UserLoaded(users));
});
}
}

判断基準:

ライブラリ学習コスト型安全性適用範囲推奨度
Provider低い中程度小〜中規模⭐⭐⭐⭐
Riverpod中程度高い中〜大規模⭐⭐⭐⭐⭐
Bloc高い高い大規模⭐⭐⭐
GetX低い低い小規模⭐⭐

実践的な選択指針:

// 小規模プロジェクト: Provider
// - シンプルで学習コストが低い
// - 迅速な開発が可能
// 中規模プロジェクト: Riverpod
// - 型安全性が高い
// - コンパイル時にエラーを検出
// 大規模プロジェクト: Bloc
// - 複雑な状態遷移に対応
// - イベント駆動の設計

Flutterアプリケーションのアーキテクチャ設計において重要なポイント:

  1. レイヤー分離: UI、ビジネスロジック、データアクセスを明確に分離
  2. 状態管理: プロジェクト規模に応じた適切な状態管理ライブラリの選択
  3. テスト容易性: 各レイヤーを独立してテストできるように設計
  4. スケーラビリティ: プロジェクトの成長を見据えた設計

シニアエンジニアとして考慮すべき点:

  1. プロジェクト規模: 小規模ならシンプルに、大規模なら構造化
  2. チームのスキル: チーム全体が理解できるアーキテクチャを選択
  3. 将来の拡張性: プロジェクトの成長を見据えた設計
  4. 一貫性: プロジェクト全体で統一されたアーキテクチャを維持