MVVMとは?Flutter(DartPad)で動く最小デモとシーケンス図で理解する
この記事では MVVM(Model–View–ViewModel) について、
概念説明 → 動くデモコード → 図解(シーケンス図・構造図) の順で解説します。
Flutter × DartPad(ブラウザSandbox)を使い、
環境構築なし・1ファイル完結で理解できる構成にしています。
MVVMとは?
MVVMは、UIアプリケーションを次の3つの役割に分離する設計パターンです。
-
Model
アプリが扱うデータ・状態(例:カウント値、ユーザー情報) -
View
画面(FlutterでいうWidget)。表示とユーザー操作のみを担当 -
ViewModel
ViewとModelの橋渡し役。状態管理・ロジックを担当
一言でいうと
UI(View)からロジックを切り離し、画面を薄く保つための設計。
なぜMVVMを使うのか?
メリット
- UIとロジックの責務が分離され、コードが読みやすい
- ViewModelはUIに依存しないためテストしやすい
- UI変更(iOS / Android / Web)に強い
デメリット
- 小規模アプリでは分割が過剰に感じることがある
- 責務を意識しないと「なんちゃってMVVM」になりがち
今回のデモでやること
- カウント表示
-
+1ボタンでカウント増加 -
resetボタンで初期化
※ DartPad(Flutter)は 実質1ファイルのみ扱えるため、
今回は 「1ファイルMVVM(擬似MVVM)」 として実装します。
デモコード(DartPad対応・1ファイルMVVM)
import 'package:flutter/material.dart';
void main() => runApp(const App());
/// =======================
/// Model(状態データ)
/// =======================
@immutable
class CounterState {
final int count;
const CounterState({required this.count});
CounterState copyWith({int? count}) {
return CounterState(count: count ?? this.count);
}
}
/// =======================
/// ViewModel(状態管理・ロジック)
/// =======================
class CounterViewModel extends ChangeNotifier {
CounterState _state = const CounterState(count: 0);
CounterState get state => _state;
void increment() {
_state = _state.copyWith(count: _state.count + 1);
notifyListeners();
}
void reset() {
_state = const CounterState(count: 0);
notifyListeners();
}
}
/// =======================
/// View(UI)
/// =======================
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: CounterPage(),
);
}
}
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
final vm = CounterViewModel();
@override
void dispose() {
vm.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MVVM Demo (DartPad)')),
body: Center(
child: AnimatedBuilder(
animation: vm,
builder: (context, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'count: ${vm.state.count}',
style: const TextStyle(fontSize: 32),
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: vm.increment,
child: const Text('+1'),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: vm.reset,
child: const Text('reset'),
),
],
),
],
);
},
),
),
);
}
}
MVVMのデータフローをシーケンス図で理解する
MVVMでは、データと処理の流れが一方向になることが重要です。
ここでは「ボタンを押してカウントを1増やす」ケースを例に、
MVVM内部で何が起きているのかをシーケンス図で見ていきます。
基本的な流れ
- ユーザーが画面(View)を操作する
- View が ViewModel のメソッドを呼び出す
- ViewModel が Model(状態)を更新する
- ViewModel が View に変更を通知する
- View が最新の状態を使って再描画される
ポイントは、View が直接 Model を変更しないことです。
シーケンス図(Mermaid)
図から読み取れること
上記のシーケンス図から、MVVMの設計上の重要なポイントが読み取れます。
-
View はユーザー操作の受付に専念している
ボタンタップなどのイベントを受け取り、
ロジックは持たず ViewModel に処理を委譲している。 -
ViewModel が状態変更の唯一の窓口になっている
状態(Model)の更新は必ず ViewModel を経由するため、
ビジネスロジックの所在が明確になる。 -
Model は純粋なデータとして扱われている
Model 自身は振る舞いを持たず、
View や ViewModel から独立した存在になっている。 -
状態変更は通知を通じて View に伝播する
ViewModel が状態更新後に通知を出し、
View はそれをトリガーに再描画される。 -
データフローが一方向である
View → ViewModel → Model → View という流れが守られ、
双方向依存や直接参照が発生していない。
これらのポイントにより、
- UI とロジックが疎結合になる
- 影響範囲が限定され、変更に強くなる
- ViewModel を単体でテストしやすくなる
といった MVVM のメリットが得られます。