はじめに
MVP や MVVM の進化形とも言える MVI は、単方向データフロー(Unidirectional Data Flow, UDF) を重視し、UI を「状態の関数」として設計します。
MVI の3つの要素
MVI は以下の3つで構成されます。
1. Model(状態)
アプリケーションの状態(State)を表す。
Immutable(不変)なデータとして定義し、UI はこの状態を監視して描画します。
data class UiState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String? = null
)
2. View(UI)
状態を受け取り、描画する役割。
ユーザー操作は Intent として ViewModel に送ります。
@Composable
fun SampleScreen(state: UiState, onIntent: (UiIntent) -> Unit) {
if (state.isLoading) {
CircularProgressIndicator()
} else {
LazyColumn {
items(state.items) {
Text(it)
}
}
}
}
3. Intent(ユーザー操作)
ユーザーの操作やイベントを表す。
「こういうことをしたい」という意思を ViewModel に伝えるためのもの。
sealed class UiIntent {
object LoadData : UiIntent()
data class ClickItem(val id: Int) : UiIntent()
}
データフロー
MVI の最大の特徴は 単方向データフロー です。
ユーザー操作 → Intent → Reducer(ViewModel) → 新しい State → View(UI)
- View がユーザー操作を Intent として ViewModel に渡す
- ViewModel が Reducer で新しい State を計算
- State が更新されると View が再描画
このループにより、UI は 常に State に依存する純粋関数 のように振る舞います。
MVI のメリット
- 状態の一元管理 → バグを減らせる
- 一方向のデータフロー → デバッグが容易
- 過去の State を保存すれば タイムトラベルデバッグ が可能
- View がシンプル → テストがしやすい
MVI のデメリット
- State の定義が膨大になりがち
- Reducer の設計が複雑化する可能性
- 全てを State に含めるため メモリコスト が増えることもある
他のアーキテクチャとの比較
| パターン | 特徴 |
|---|---|
| MVP | Presenter がロジックを担当。状態は明示的に管理されない |
| MVVM | ViewModel が双方向データバインディングを行う場合が多い |
| MVI | Intent → State → View の 一方向フロー に限定 |
Flutter/Compose での実装例
Flutter の場合は以下のように整理すると良いです。
-
UiIntent:sealed class(Dartならfreezedやsealed_unionsを利用) -
UiState:freezedで生成 -
ViewModel:StateNotifier(Riverpod)を利用して Reducer を実装
@freezed
class UiState with _$UiState {
const factory UiState({
@Default(false) bool isLoading,
@Default([]) List<String> items,
String? error,
}) = _UiState;
}
@freezed
class UiIntent with _$UiIntent {
const factory UiIntent.loadData() = LoadData;
const factory UiIntent.clickItem(int id) = ClickItem;
}
class UiViewModel extends StateNotifier<UiState> {
UiViewModel() : super(const UiState());
void onIntent(UiIntent intent) {
intent.when(
loadData: _loadData,
clickItem: _onClick,
);
}
void _loadData() {
state = state.copyWith(isLoading: true);
// API呼び出しなど…
}
void _onClick(int id) {
// クリック処理
}
}
まとめ
- MVI は 状態駆動のUI を実現するためのパターン
- 単方向データフローで構造がシンプルになり、デバッグやテストが容易
- 特に Jetpack Compose, Flutter, React のような宣言的 UI と相性抜群