💬

「MVVM=双方向データバインディング」じゃない!Flutterで正しく理解するMVVM

に公開

はじめに

株式会社ASSIGN(アサイン)の白井です。
現在、転職者向けのモバイルアプリ開発を担当しています。

今年はFlutterのテックリードを目指して日々学習を進めていますが、その中で「MVVM=双方向データバインディングができるアーキテクチャ」という誤解をしていたことに気づきました。

学習を重ねるにつれ、Flutterはそもそも単方向データフロー(Unidirectional Data Flow)を前提としたフレームワークであり、Android Frameworkのような双方向データバインディングを前提とする構造とは本質的に異なることを理解しました。

Flutter × MVVM に関する記事も多く存在しますが、その中には、MVVMを採用するメリットが双方向データバインディングにあるかのように表現されているものも散見されます。

今回は、そうした誤解をFlutterの設計思想に沿って紐解いていければと考えています。

日々Flutterでモバイルアプリを開発されている方、またこれからFlutterでの開発を始めようとしている方にとって、設計やアーキテクチャを考える際の一助となれば幸いです。

MVVMとは何か?

そもそもMVVMとは何なのでしょうか?

MVVM(Model–View–ViewModel)は、UIとロジックを分離し、表示(View)・状態とプレゼンテーションロジック(ViewModel)・ドメイン/データ(Model)の責務を明確にするためのアーキテクチャパターンです。
ポイントは「UIは状態の結果として描画される」という考え方に立ち、ViewModelが単一の真実の状態(Single Source of Truth)を提供することです。

つまりMVVMとは、
「アプリの状態を中心に、UIとロジックを分離して構築するための設計思想」です。

役割と責務

  • Model

    • ドメイン知識・エンティティ・リポジトリ、外部I/O(API/DB等)を扱う層
    • できるだけUIフレームワーク非依存
    • 不変(Immutable)な値オブジェクトやエンティティを中心に据える
  • View

    • 画面(ウィジェット)の描画のみ
    • ViewModelの「公開状態」を購読して描画を更新
    • ユーザー操作(Intent)をViewModelに伝えるだけ(ロジックは持たない)
  • ViewModel

    • 入力(ユーザー操作)を受け取り、ユースケース/リポジトリを呼び出す
    • UI向けに整形された状態(例:ViewState)を公開する
    • 副作用はユースケース/リポジトリへ委譲し、UIは状態の変化だけを監視する

概念図

データバインディングを正しく理解する

次に、双方向データバインディングを一方向データバインディングと対比しながら整理していきましょう。

双方向データバインディングとは

双方向データバインディング(Two-way Data Binding)とは、「アプリの設計思想」や「アーキテクチャ」ではなく、フレームワークが提供する同期機能のひとつで、UI(View)とデータ(Model)が互いに状態を更新し合う仕組みのことです。

アプリの中で、ユーザーが画面上の入力フィールドを編集すると、その値が自動的にデータ(Model)へ反映され、逆にModel側のデータがアプリ内部の処理や外部APIのレスポンスなどで更新された場合には、UIがその変更を検知して即座に最新の状態に再描画されます(=Model → View)。

つまり、UIとデータの間で「双方向の同期」が常に保たれている状態です。
開発者が「取得処理 → UI更新」といったコードを都度書かなくても、バインディングの仕組みが自動的にデータの変更をUIへ反映し、UI操作をデータに戻すため、ユーザーが常に最新の状態を見られるようになります。

概念図

一方向データバインディングとは

一方向データバインディング(One-way Data Binding)とは、アプリ内で「データの流れが一方向(Model → View)」に限定されるデータバインディングの仕組みを指します。

たとえば、ユーザー情報などのModelが更新されると、その変更が自動的にUIに反映されますが、UI側(View)での操作は直接Modelを変更しません。

UIで変更を加えたい場合は、イベント(ボタンタップやフォーム入力など)をトリガーとして
「意図的に」Modelを更新するロジックを明示的に書く必要があります。

このように、「データは一方向に流れ、UIの変更はイベント経由でModelに伝わる」という構造を持つのが、一方向データバインディングです。

概念図

よくある誤解:一方向データバインディングと単一データフローの違い

ここで混同されやすいのが、「一方向データバインディング(One-way Data Binding)」と「単一データフロー(Unidirectional Data Flow)」の関係です。
この2つは似た言葉ですが、まったく異なる概念です。

一方向データバインディングは、
「データが Model → View の一方向に流れる仕組み(メカニズム)」を指します。
つまり、UIの描画はデータの状態に従って行われますが、UIからModelへ変更を加える場合は、イベントを通じて明示的に処理する必要があります。

一方、単一データフローは、「アプリ全体の状態管理を、常にひとつの方向に流れるように設計する思想(アーキテクチャ思想)」です。
状態の変化が親から子へと一方向に伝播し、逆方向の更新(子から親)を直接的に行わないようにすることで、アプリ全体のデータフローを一貫して制御します。

両者の関係を整理すると
観点 一方向データバインディング 単一データフロー
概念の種類 仕組み(メカニズム) 設計思想(アーキテクチャ)
対象範囲 View と Model の関係 アプリ全体の状態管理
データの流れ Model → View の片方向 親 → 子 の一方向(アプリ全体)
更新方法 イベント経由で Model を更新 状態を上位で管理し、下位に伝播
採用例 Flutter、React、Svelte React、Redux、Flux など

つまり、

一方向データバインディングは「データの流れ方を定義する仕組み」
単一データフローは「アプリ全体の設計思想」

と言えると思います。

双方向データバインディングと一方向データバインディングの違い

上記それぞれの説明で触れたように、一方向データバインディング(One-way Data Binding)と双方向データバインディング(Two-way Data Binding)は、どちらも「データとUIを自動で結びつける仕組み」ですが、データの流れの方向が根本的に異なります。

双方向データバインディングでは、UIとデータが常に同期しており、Model ↔ View の両方向に状態が伝わります。
一方で、一方向データバインディングは、Model → View という流れに限定し、データの変更がUIへ伝わるのみで、UI側の変更はイベント経由で明示的にModelへ伝える構造です。

つまり、「UIとデータの同期性を重視する仕組み」が双方向データバインディング、「データの流れを一方向に制御し、状態の追跡を容易にする仕組み」が一方向データバインディング です。

観点 双方向データバインディング 一方向データバインディング
データの流れ Model ↔ View(相互更新) Model → View(片方向)
更新の仕組み フレームワークが自動で同期 イベント経由で明示的に更新
Model更新時 UIが自動的に再描画される UIが自動で再描画される
View更新時 入力内容がModelに即反映される イベントを介してModelを更新
メリット コード量が少なく、開発が容易 状態の流れが明確で、バグを追いやすい
デメリット 状態の同期が複雑化しやすい 明示的な処理が増える(手間)
採用例 Vue.js(v-model)、Angular(ngModel) React、Flutter、Svelte

MVVM = 双方向データバインディングという誤解

ここからは、Flutterを通して「MVVM=双方向データバインディング」という誤解を解いていきましょう。

改めて先述の説明を振り返ると、MVVM(Model–View–ViewModel)は、UI(View)とビジネスロジック(Model)を分離し、保守性や再利用性を高めるための設計パターンです。

ViewはUIの描画とユーザー操作を担い、ViewModelはUIに表示するための状態や処理を仲介し、Modelはアプリケーションのビジネスロジックやデータ操作を担います。
このように、各層が明確な責務を持つことこそがMVVMの目的です。

つまり、MVVMは「構造(パターン)」を定義するものであり、データの同期や更新方法そのものを規定するものではありません。
データバインディングの仕組みは、あくまで利用するフレームワークが提供する機能によって異なります。

「MVVMを採用したから自動でデータが同期される」わけではなく、
フレームワークによっては、MVVMの実装を支援するためにデータバインディング機能を提供しているだけという理解が非常に重要です。

観点 内容
MVVM(パターン) Viewとロジックを分離するための設計上の考え方(責務分離の構造)
データバインディング(機能) ViewとViewModelの値を自動的に同期させる仕組み(フレームワーク依存の実装手段)

実例:Flutter × MVVM

ここでは、Flutterアプリでユーザー情報(氏名・年齢)の表示と更新を行う画面を例に、MVVMの考え方を具体的に見ていきましょう。

以下ユーザープロフィール画面で氏名と年齢を表示、更新できる画面のUI実装です。

class UserProfile extends HookConsumerWidget {
  const UserProfile({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(userProfileViewModelProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('ユーザープロフィール')),
      body: userState.when(
        data: (state) => Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('名前: ${state.name}', style: const TextStyle(fontSize: 18)),
              Text('年齢: ${state.age}', style: const TextStyle(fontSize: 18)),
              const SizedBox(height: 16),
              TextField(
                decoration: const InputDecoration(labelText: '名前を変更'),
                onChanged: (value) =>
                    ref.read(userProfileViewModelProvider.notifier).updateName(value),
              ),
            ],
          ),
        ),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (err, _) => Center(child: Text('エラー: $err')),
      ),
    );
  }
}

上記の例では、ユーザーが入力フィールドに文字を入力すると onChanged が呼ばれ、
その値が ViewModel に明示的に渡されます。

Flutterでは、状態管理の原則として 単方向データフロー(Unidirectional Data Flow) が採用されているため、データの流れは次のように整理できます。

  1. Model → View(自動)
    • 状態(State)が変化すると、Flutterが内部でWidgetツリーを再構築し、「この状態ならUIはこうあるべき」という宣言に基づいて自動的に再描画します。
    • 開発者が「再描画処理」を書く必要はありません。
  2. View → Model(明示的)
    • 一方で、UI側から状態を変更したい場合は、onChanged や onPressed のようなイベントを経由して、「どのタイミングで何を更新するか」を明示的に記述します。
    • 開発者が手動で「状態変更の契機」を定義する必要があります。

このように、更新処理は自動的にバインディングされていないため、たとえMVVMを採用していても、開発者は「ユーザー操作をどのようにデータへ反映させるか」を明示的に定義する必要があります。

FlutterでMVVMを採用するメリット

MVVMを採用するメリット・採用基準

MVVMを採用するメリット

  • 責務分離が明快
    • Viewは「見た目とイベント通知」に専念し、ViewModelが「状態・処理」を一手に担うため、画面コードの肥大化(Massive Widget)を避けやすい。
  • 参照情報が豊富で学習コストが低い
    • MVVMは記事・サンプル・議論が多く、初期導入でつまずきにくい。
    • チームにとって「理解の共通土台」を作りやすい。
  • 小規模・短納期にフィット
    • 画面単位でViewModelを割り当てるだけでも一定の秩序が生まれるため、小規模アプリやPoCでの初動が速い。

ポイント:
Flutterでは双方向データバインディングは前提ではないため、
「MVVM=双方向同期」ではなく「MVVM=責務分離」として捉えるとズレにくい。

MVVMを採用する基準

  • アプリ規模が小〜中規模である

    • 機能数や画面数が限られ、状態の依存関係が比較的単純な場合に適している。
    • 機能単位でViewModelを分離するだけでも構造が整理され、十分に効果を発揮する。
  • チーム内にFlutter初心者〜中級者が多い

    • MVVMは理解しやすく、参考記事や実装例も豊富なため、チーム内で共通言語として扱いやすい。
    • 状態管理ライブラリ(Riverpod、BLoCなど)の詳細を知らなくても開発を進めやすい。
  • 短期間での開発・検証フェーズである

    • MVPやPoCなど、まずはスピード重視で形にしたいフェーズでは、MVVMの導入コストが低く素早く構築できる。
    • 後からアーキテクチャをリファクタリングする前提での仮採用にも向いている。

最後に

本記事では、「MVVM=双方向データバインディング」ではないという誤解を解きながら、
MVVMの本質である「責務分離」という構造と、Flutterが採用する宣言的UIおよび単方向データフロー(Unidirectional Data Flow)の関係について整理しました。

Flutterでは、状態(State)がUIを決定し、ユーザー操作は明示的なイベントを通じて状態を更新するという一方向の流れが、シンプルで予測可能なアプリケーション構造を実現しています。
つまり、MVVMの目的である「見通しのよい責務分離」は、双方向データバインディングを使わなくても十分に達成できるのです。

アーキテクチャの形に正解はありませんが、まずは仕組みや思想を正しく理解することが、より良い設計を選ぶ第一歩になると感じています。

今後は、今回整理した概念を踏まえ、Flutterにおける実践的なMVVMアーキテクチャ設計や、
Riverpodを用いた責務分離の具体的な実装例なども別記事で掘り下げていく予定です。

本記事が、Flutterのアーキテクチャや設計思想を見直すきっかけとなり、
日々の開発の中で「なぜこの構造なのか?」を考える一助になれば幸いです。

最後まで読んでいただき、ありがとうございました。

参考リンク・関連資料

以下は本記事執筆時点で参考にした記事や、アーキテクチャ/Flutter設計の考えを深める上で有用な資料です。

⚠️ 注意
これらのリンクは「理解を深めるための補助資料」であり、本文で述べた設計方針や考察をそのまま支持しているとは限りません。
各資料の文脈・意図を読み解いたうえで参考にしていただくことをおすすめします。

アサイン

Discussion