
アプリが大きくなり始めたとき、最初に効いてくるのが設計とディレクトリ構成です。
これは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を細かく分けすぎず、
ViewModelと
Repositoryの分離ができていれば十分実務に耐えます。
// 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では、この構成を前提に実装速度を一気に上げていきます。
0 件のコメント:
コメントを投稿