シリーズ最終回です。
これまで学んだ知識を組み合わせて、アプリ全体の構造を整理する方法を解説します。
目次
これまでのおさらい
第1回:Providerはデータ倉庫。バケツリレーが不要になる
第2回:ChangeNotifier + notifyListeners() で画面が自動更新される
第3回:context.watch(表示)と context.read(処理)を使い分ける
今回は 「アプリ全体をどう構成するか」 という設計の話です。
MultiProvider とは?
Providerが1つだけなら、こう書けます。
void main() => runApp(
ChangeNotifierProvider(
create: (_) => StudentViewModel(),
child: MyApp(),
),
);
でもアプリが大きくなると、Providerは必ず複数になります。
// Providerが増えると…
void main() => runApp(
ChangeNotifierProvider(
create: (_) => StudentViewModel(),
child: ChangeNotifierProvider(
create: (_) => StampViewModel(),
child: ChangeNotifierProvider(
create: (_) => CalendarViewModel(),
child: MyApp(), // どんどん深くなる😱
),
),
),
);
これを解決するのが MultiProvider です。
// MultiProvider で整理する
void main() => runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => StudentViewModel()),
ChangeNotifierProvider(create: (_) => StampViewModel()),
ChangeNotifierProvider(create: (_) => CalendarViewModel()),
],
child: MyApp(),
),
);
フラットに書けてスッキリします!
MVVM設計って何?
Model・View・ViewModel の3つに役割を分ける設計パターンです。
┌─────────────────────────────────────────┐
│ View(画面・Widget) │
│ → 表示するだけ。ロジックは書かない │
└────────────┬────────────────────────────┘
│ 監視・通知
┌────────────▼────────────────────────────┐
│ ViewModel(ChangeNotifier) │
│ → ビジネスロジック・状態管理 │
└────────────┬────────────────────────────┘
│ データ操作
┌────────────▼────────────────────────────┐
│ Model(データクラス・Firestore等) │
│ → データの形・保存・取得 │
└─────────────────────────────────────────┘
月謝袋アプリで例えると
View → 画面に生徒リストを表示する
ViewModel → Firestoreからデータを取得・追加・削除する処理
Model → 「生徒」というデータの形(名前・月謝など)
3つの役割をコードで見る
Model(データの形)
// student.dart
class Student {
final String id;
final String name;
final int monthlyFee;
final String color; // 生徒ごとのカラー
Student({
required this.id,
required this.name,
required this.monthlyFee,
required this.color,
});
// Firestoreのデータ → Studentクラスに変換
factory Student.fromFirestore(Map<String, dynamic> data, String id) {
return Student(
id: id,
name: data['name'] ?? '',
monthlyFee: data['monthlyFee'] ?? 0,
color: data['color'] ?? '#FF0000',
);
}
}
ViewModel(ロジック担当)
// student_view_model.dart
class StudentViewModel extends ChangeNotifier {
final FirestoreService _firestoreService;
StudentViewModel(this._firestoreService);
List<Student> _students = [];
List<Student> get students => _students;
bool _isLoading = false;
bool get isLoading => _isLoading;
// Firestoreから生徒一覧を取得
Future<void> fetchStudents() async {
_isLoading = true;
notifyListeners();
_students = await _firestoreService.getStudents();
_isLoading = false;
notifyListeners(); // 取得完了 → 画面更新
}
// 生徒を追加
Future<void> addStudent(String name, int fee) async {
await _firestoreService.addStudent(name, fee);
await fetchStudents(); // 追加後に再取得 → notifyListeners()が呼ばれる
}
}
View(表示担当)
// student_list_page.dart
class StudentListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = context.watch<StudentViewModel>();
// ローディング中はインジケーターを表示
if (vm.isLoading) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: vm.students.length,
itemBuilder: (_, index) => ListTile(
title: Text(vm.students[index].name),
subtitle: Text('月謝:¥${vm.students[index].monthlyFee}'),
),
);
}
}
Providerが増えてきたら globalProviders で整理する
Providerが増えてきたら、main.dart に全部書くのではなく、別ファイルに切り出します。
providers.dart(Providerをまとめるファイル)
// 土台となるサービス(誰にも依存しない)
List<SingleChildWidget> independentModels = [
Provider(create: (_) => FirestoreService()),
Provider(create: (_) => LocalStorageService()),
];
// サービスを使うもの(independentModelsに依存する)
List<SingleChildWidget> dependentModels = [
ProxyProvider<FirestoreService, StudentRepository>(
update: (_, firestoreService, __) =>
StudentRepository(firestoreService),
),
];
// 画面に直接渡すViewModel(dependentModelsに依存する)
List<SingleChildWidget> viewModels = [
ChangeNotifierProxyProvider<StudentRepository, StudentViewModel>(
create: (_) => StudentViewModel(),
update: (_, repo, vm) => vm!..updateRepository(repo),
),
];
// 3つをまとめる(順番が大事!)
List<SingleChildWidget> globalProviders = [
...independentModels, // ① まず土台
...dependentModels, // ② 土台を使うもの
...viewModels, // ③ 最後に画面用
];
main.dart(スッキリ!)
void main() => runApp(
MultiProvider(
providers: globalProviders,
child: MyApp(),
),
);
なぜ順番が大事なの?
List<SingleChildWidget> globalProviders = [
...independentModels, // ① FirestoreService を登録
...dependentModels, // ② FirestoreService を使うものを登録
...viewModels, // ③ その次に画面用を登録
];
Providerは 上から順番に登録 されます。
// ❌ NG:順番が逆だとエラーになる
List<SingleChildWidget> globalProviders = [
...viewModels, // StudentRepository がまだ存在しない!😱
...dependentModels, // FirestoreService がまだ存在しない!😱
...independentModels,
];
「使う側」より「使われる側」を先に登録する、というルールです。
フォルダ構成のお勧め
lib/
├── main.dart ← MultiProvider(globalProviders)
│
├── providers.dart ← globalProviders をまとめる
│
├── models/
│ └── student.dart ← データの形
│
├── view_models/
│ └── student_view_model.dart ← ロジック
│
└── views/
└── student_list_page.dart ← 画面
役割ごとにフォルダを分けると、どこに何があるか一目でわかります。
シリーズ全体のまとめ
| 回 | テーマ | ポイント |
|---|---|---|
| 第1回 | Providerとは | データ倉庫。バケツリレーが不要になる |
| 第2回 | ChangeNotifier | notifyListeners()で画面が自動更新 |
| 第3回 | watch と read | 表示はwatch・処理はread |
| 第4回 | MultiProvider + MVVM | 役割を分けてアプリを整理する |
最後に
Providerは最初は難しく感じますが、「データを空中に置いて、どこからでも取り出す」「変わったら自動で画面に伝える」 というたった2つのことをやっているだけです。
この仕組みを理解してしまえば、画面が更新されない問題や、データの受け渡しで悩むことがぐっと減ります。
Flutter / Provider / MultiProvider / MVVM / 初心者 / アプリ開発
