Flutter アーキテクチャ徹底比較!MVC vs MVVM vs クリーンアーキテクチャ
アプリの規模が大きくなり、機能が増え、チームでの開発が必要になった時、適切なアーキテクチャの導入は不可欠です。
この記事では、Flutter開発でよく議論される3つの主要なアーキテクチャパターン「MVC」「MVVM」「クリーンアーキテクチャ」を比較し、それぞれの特徴と、どのようなプロジェクトに向いているのかを解説します。
なぜアーキテクチャが必要なのか?
比較に入る前に、なぜアーキテクチャが必要なのかを再確認しましょう。主な目的は以下の通りです。
- 関心の分離 (Separation of Concerns): UI描画ロジックと、ビジネスロジック(データの処理、通信など)を切り離すことで、コードの見通しを良くします。
- 保守性の向上 (Maintainability): コードが整理されているため、バグ修正や機能追加が容易になります。
- テスト容易性 (Testability): ビジネスロジックがUIから切り離されているため、ユニットテスト(単体テスト)が書きやすくなります。
FlutterのWidgetは強力で柔軟ですが、油断するとUIコードとロジックが混在しがちです。アーキテクチャは、この混沌に秩序をもたらすためのルールです。詳細については、Flutter公式ドキュメントの「アーキテクチャの概要」も参照してください。
1. MVC (Model-View-Controller)
MVCは、歴史が古く、最も基本的なUIアーキテクチャパターンです。Webフレームワークなどで広く採用されてきました。
構造
- Model (モデル): アプリケーションのデータとビジネスロジックを担当します。データベース、APIとの通信などもここに含まれます。
- View (ビュー): ユーザーインターフェース (UI) を担当します。Modelのデータを画面に表示し、ユーザーの入力を受け付けます。FlutterではWidgetがこれに当たります。
- Controller (コントローラー): ViewとModelの橋渡し役です。Viewからユーザーの入力を受け取り、Modelを更新するよう指示を出します。
FlutterにおけるMVC
Flutterの標準的な StatefulWidget は、構造的にMVCに近い性質を持っています。
Widgetクラスが View の定義Stateクラスが Controller (ロジック) と View (buildメソッドによる描画) の両方の役割を担う
しかし、厳密なMVCをFlutterで実現しようとすると、「Controller」の役割が曖昧になりがちです。Flutterの宣言的UIパラダイムにおいては、後述するMVVMの方が自然にフィットします。
メリットとデメリット
- メリット:
- デメリット:
2. MVVM (Model-View-ViewModel)
現在、Flutter開発において最も主流かつ推奨されるアーキテクチャパターンです。
構造
- Model: MVCと同じく、データ構造とビジネスロジック(データの取得・保存など)を担当します。
- View: UI (Widget) を担当します。ユーザーの操作をViewModelに伝えます。重要なのは、ViewがViewModelの状態を監視(バインド)し、状態が変化したら自動的に再描画される点です。
- ViewModel: Viewのための「状態」保持と、その状態を変更するロジックを担当します。Modelからデータを取得し、Viewが表示しやすい形に加工して保持します。ViewModelはViewへの参照を一切持ちません。
FlutterにおけるMVVMと実装例
Flutterの「宣言的UI」と非常に相性が良いのが特徴です。ViewModelが「状態」を更新し、それを監視しているWidgetが自動的に再ビルドされるという流れが自然に作れます。
このパターンを実現するために、以下のような状態管理ライブラリが広く使われています。
- Riverpod: 現在最も人気があり、推奨されるライブラリ。
NotifierやAsyncNotifierがViewModelの役割を果たします。 - Provider: Riverpodの前身であり、現在も広く使われている公式推奨ライブラリ。
ChangeNotifierがViewModelの役割を果たします。 - Flutter Bloc: より厳格なルールに基づいた状態管理を提供します。
BlocやCubitがViewModelに相当します。
簡易コード例(Riverpodを用いたMVVM)
以下は、Riverpodを使って「カウンターアプリ」をMVVMで実装した場合の最小限のイメージです。ロジックがWidgetから完全に切り離されている点に注目してください。
ViewModel (CounterNotifier.dart)
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ViewModel: 状態(int)を保持し、状態を変更するロジックを持つ
class CounterNotifier extends Notifier<int> {
@override
int build() {
return 0; // 初期値
}
// ロジック: 状態を更新する
void increment() {
state++;
}
}
// Provider: ViewModelをアプリ全体に提供する定義
final counterProvider = NotifierProvider<CounterNotifier, int>(() {
return CounterNotifier();
});
View (CounterScreen.dart)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'counter_notifier.dart'; // 上記のファイルをインポート
// View: UIの定義のみを行う。ロジックは持たない。
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ViewModelの状態を監視 (バインド)。状態が変わるとここが再実行される。
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('MVVM Counter')),
body: Center(
child: Text('$count', style: Theme.of(context).textTheme.headlineMedium),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// ViewModelのメソッドを呼び出してロジックを実行する
ref.read(counterProvider.notifier).increment();
},
child: const Icon(Icons.add),
),
);
}
}
メリットとデメリット
- メリット:
- デメリット:
3. クリーンアーキテクチャ (Clean Architecture)
クリーンアーキテクチャは、Robert C. Martin氏によって提唱された、MVCやMVVMのような単なるUIパターンではなく、アプリケーション全体の構造を定義する、より抽象度の高い設計思想です。
核心は依存性のルール (Dependency Rule) です。
- 「同心円」の外側から内側に向かってのみ依存関係が発生する。
- 内側の円(ビジネスロジック)は外側の円(UIやDB、フレームワーク)について何も知らない。
構造 (Flutterでの一般的な層分け)
Flutterでは通常、以下の3層(または4層)に分けて実装されます。
- Presentation Layer (プレゼンテーション層 - 外側):
- Domain Layer (ドメイン層 - 最も内側):
- Data Layer (データ層 - 外側):
Flutterにおけるクリーンアーキテクチャの適用
重要かつ誤解されやすい点ですが、クリーンアーキテクチャとMVVMは対立するものではありません。
通常、クリーンアーキテクチャの「Presentation Layer」の中で、UIパターンの実装としてMVVM(RiverpodやBloc)が採用されます。つまり、MVVMを包含する、より大きな設計の枠組みがクリーンアーキテクチャです。
メリットとデメリット
- メリット:
- デメリット:
比較まとめ
| 特徴 | MVC | MVVM (推奨) | クリーンアーキテクチャ |
|---|---|---|---|
| 主な関心事 | UIとロジックの単純な分離 | 状態管理とUIの分離 (リアクティブ) | アプリ全体の依存関係の整理と独立性 |
| 複雑さ | 低 | 中 | 高 |
| テスト容易性 | △ (結合度が高い) | ◎ (ViewModelのテストが容易) | ☆ (ドメインロジックが完全に独立) |
| 実装コスト | 低 | 中 | 高 (ボイラープレートが多い) |
| Flutterとの相性 | △ | ◎ (現在の主流) | ◯ (大規模・長期運用向け) |
| 向いているPJ | 超小規模、プロトタイプ | 小〜中規模、一般的な商用アプリ | 大規模、数年単位の長期運用、エンタープライズ |
結論:どれを選ぶべきか?
1. 基本は MVVM (Riverpod, Blocなど) を目指す
ほとんどのFlutterプロジェクトにとって、MVVMは実装コストと保守性のバランスが最も取れた最適解です。まずは標準の setState を卒業し、「Riverpod」などのライブラリを使ってロジックとUIを分離することを徹底しましょう。これだけで十分なテスト容易性と保守性が得られます。
2. 大規模プロジェクト、またはコアロジックが非常に複雑な場合はクリーンアーキテクチャを検討する チームメンバーが多数いる、数年単位で運用する、または金融系アプリのようにビジネスロジックが極めて重要で複雑な場合は、クリーンアーキテクチャを採用する価値があります。ただし、初期の学習コストと実装コストは覚悟する必要があります。
3. MVCは意識して選ぶ必要はない Flutterにおいて、あえて古典的な「MVCパターン」を意識して実装することは推奨されません。MVVMを目指して状態管理ライブラリを適切に使えば、自然とMVCが解決しようとしていた問題は解消されます。
最後に
アーキテクチャは手段であり、目的ではありません。「クリーンアーキテクチャを採用しているから良いアプリだ」というわけではありません。
プロジェクトの規模、チームのスキル、将来の展望(運用期間や変更の可能性)に合わせて、最適なアーキテクチャを選択してください。