ViewModel が肥大化する理由

Android アプリを作っていると、ある日ふと気づきます。

ViewModel、でかくなりすぎじゃない?

State の定義、Flow の合成、エラーハンドリング、変換ロジック、画面固有の分岐……。 気づけば 1 ファイルに数百行。しかも「どこを直すと何が壊れるのか分からない」状態。

この記事では、なぜ ViewModel は肥大化しやすいのか、そして それを防ぐための本質的な境界の引き方 について整理します。


よくある誤解:ViewModel は「中継役」だから重くなる

よく言われる説明に、こんなものがあります。

  • ViewModel は UI と Domain の橋渡しだから
  • Flow や State を扱うから
  • 非同期処理が集まりやすいから

どれも一理ありますが、本当の理由ではありません

Flow があるから肥大化するのではなく、 ViewModel が 本来持つべきでない判断 を持ち始めたときに肥大化します。


ViewModel が肥大化する本当の理由

結論から言うと理由はシンプルです。

  • 「何をするか」を ViewModel が決め始めるから

本来の ViewModel の役割は、次の 2 つです。

  • UI からのイベントを受け取る
  • UI が描画しやすい State に変換して公開する

ところが実際には、次のような責務が入り込みがちです。

  • この操作は許可すべきか?
  • このデータは保存すべきか?
  • A のときは B、C のときは D
  • Entity をどう解釈すべきか?

これらはすべて UI の問題ではありません


「UI の判断」と「業務の判断」は別物

ViewModel が持つべき判断は、あくまで UI に閉じたものです。

  • ローディングを表示するか
  • エラーメッセージをどう出すか
  • ボタンを活性にするか

一方で、次のような判断は UI の外側にあります。

  • これは作成できるデータか?
  • 更新してよい状態か?
  • 保存先はどうあるべきか?

これらは 画面が変わっても意味が変わらない判断 です。

ここを ViewModel に置くと、画面が増えるたびに同じロジックが増殖し、 結果として ViewModel が肥大化します。


UseCase が登場する理由

UseCase は、よく「処理の切り出し先」として語られますが、 本質はそこではありません。

UseCase の役割はただ一つです。

  • アプリとして「何をするか」を定義すること

ここでは、口座振替を管理するアプリの場合 を例に考えてみます。

たとえば次のようなものです。

  • 振替元の口座一覧を取得する
  • 口座振替の設定を保存する
  • 入力された金額や日付が有効かを検証する

これらはすべて、

  • どの画面から呼ばれても
  • UI が変わっても
  • 保存方法(DB / クラウドなど)が変わっても

意味が変わりません。

だから ViewModel ではなく、UseCase に置かれます。


ViewModel と UseCase の境界線

迷ったときは、次の問いを自分に投げてみてください。

  • この判断は、別の画面でも同じ意味を持つか?

YES なら UseCase。 NO なら ViewModel。

たとえば、

  • 入力が正しいか? → UseCase
  • エラーをトーストで出すか? → ViewModel

この基準で分けると、自然と ViewModel は軽くなります。


「ViewModel から処理がほとんど消えそう」問題

ここでよく出る不安があります。

UseCase に移したら、ViewModel に何も残らないのでは?

実際には、ちゃんと残ります。

  • UI イベントの受信
  • UseCase の呼び出し
  • 結果を State に詰め替える
  • UI 専用の状態管理

むしろ、ViewModel が本来の姿に戻る だけです。


まとめ

ViewModel が肥大化する原因は、

  • 技術的な問題ではなく
  • レイヤーの責務が曖昧になること

にあります。

  • ViewModel は UI の都合だけを知る
  • UseCase は アプリとして「何をするか」を知る

この境界を意識すると、

  • ViewModel は小さく保たれ
  • ロジックは再利用可能になり
  • 変更に強い構造になります

ViewModel が苦しくなってきたら、 それは 設計を見直すサイン かもしれません。