【Flutter Provider入門④】MultiProvider と MVVM設計でアプリを整理する

シリーズ最終回です。
これまで学んだ知識を組み合わせて、アプリ全体の構造を整理する方法を解説します。


目次

これまでのおさらい

第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回ChangeNotifiernotifyListeners()で画面が自動更新
第3回watch と read表示はwatch・処理はread
第4回MultiProvider + MVVM役割を分けてアプリを整理する

最後に

Providerは最初は難しく感じますが、「データを空中に置いて、どこからでも取り出す」「変わったら自動で画面に伝える」 というたった2つのことをやっているだけです。

この仕組みを理解してしまえば、画面が更新されない問題や、データの受け渡しで悩むことがぐっと減ります。


Flutter / Provider / MultiProvider / MVVM / 初心者 / アプリ開発

目次