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 が苦しくなってきたら、 それは 設計を見直すサイン かもしれません。