Jetpack Composeと(AAC) ViewModelについて、考えたことをまとめてみました。
ViewModelの役割
そもそもなぜViewModelが要るのか。それは一般的な観点でいくと、関心の分離が挙げられると思います。複数の関心は個々に切り出されるべきであり、それがコードの可読性を上げたりシンプルさを保つことに繋がります。ViewModelとViewの関心について言えば、それぞれ「状態・ロジック」と「表示」に分けられるでしょう。
また、とりわけAndroidについてはデベロッパーサイトでも触れられている通り、以下のような理由も加えられます。
Activity と Fragment の実装はデベロッパーが管理するものではないことにご注意ください。これらのクラスは、Android OS とアプリの間のコントラクトを体現する単なる結合クラスです。Android OS は、ユーザーの操作に基づいて、またはシステムの状態(メモリ不足など)を理由として、いつでもこれらのクラスを破棄できます。
これらの性質から、特に状態への依存を増やすべきでは無いです。
こうしたOSと関わりのある、Activity・Fragmentならではの事情もあったりするわけです。そういう意味でもこれらのコンポーネントでは「表示」という関心に集中するべきです(上記の通り、OSとのやりとりも含む)。
そんな中で定着しているのがAAC ViewModelなのでしょう。「状態・ロジック」と「表示」を分けるという一般的なViewModelの役割に加え、画面回転時など、Activityより長く状態を保持できる特性を活かせるシーンがあるからです。
Jetpack ComposeでもAAC ViewModelを使う?
ではViewの実装がAndroid ViewからJetpack Composeに移り変わっても、引き続きAAC ViewModelを使っていくのか?
私の今の回答としては「使ってもいいし、ViewModelの役割に相当する何かを代わりに使うでもいい」になるかなと思います。
使ってもいいと考える理由
まず、使ってもいいと考える理由はAAC ViewModelを使うメリットは依然ありそうだから、です。
例えば「状態ホルダーと UI 状態」というデベロッパーサイトのページの中にその理由があったりします。
(Jetpack ComposeとViewModelの関係性を整理する上で非常に役立つページでした。)
当ページでは、状態ホルダーを2種類に定義しています。
- ビジネスロジック状態ホルダー
- UIロジック状態ホルダー
前者の「ビジネスロジック状態ホルダー」とは何なのか。公式の説明は以下です。
ビジネス ロジック状態ホルダーはユーザー イベントを処理し、データレイヤまたはドメインレイヤのデータを画面 UI 状態に変換します。
ビジネスロジック経由で取得したデータをUIの状態として保持する役割を持つ存在、でしょうか。
サンプルコードではuiState
というプロパティとして保持していたり、またそのためのビジネスロジックを呼び出すところでもあるようです。
さらにこう続きます。
Android のライフサイクルとアプリの構成変更を検討する際に最適なユーザー エクスペリエンスを提供するために、ビジネス ロジックを利用する状態ホルダーには次の特性が必要です
必要な特性とは。
- UI 状態を生成する
- アクティビティの再作成を通して保持される
- 長期的な状態を保持する
- UI に固有で再利用できない
とのこと。
まさに、この辺りがAAC ViewModelの特性にピッタリ合うところが多いように見えます。
確かに、状態保存という観点ではrememberより長いという認識です(rememberは画面回転で死ぬはず)。
加えて、Navigationとの相性も良いです。
画面がバックスタックにある間、ナビゲーションが ViewModel をキャッシュに保存します。これは、デスティネーションに戻るときに以前に読み込んだデータをすぐに利用可能にするために重要です。コンポーザブル画面のライフサイクルに従う状態ホルダーでは困難です。
こういうキャッシング機構が無ければ、状態を必要としているComposable関数の再コンポーズが画面を戻る度に起きてしまうような気がします。そこに起因して処理によっては例えば、戻り遷移をする度に通信が発生してしまうのが微妙、といったケースも考えられます(大抵はもう取得できているはずだから)。
あとは、Hiltのサポートとも連携しているところです。AAC ViewModelではないプレーンなクラスでは、画面のコンポーザブル関数の引数に注入することが難しいorできない気がしています(ViewModelならComposable関数の引数にviewModel: MyViewModel = viewModel()
みたいなのができるはず)。
と、ViewModelのメリットを明記しつつ、同時にあることを公式は警告しています。
それは「ViewModelインスタンスを他のコンポーズ可能な関数に渡してはいけない」です。
警告: ViewModel インスタンスを他のコンポーズ可能な関数に渡さないでください。そのようにすると、コンポーズ可能な関数と ViewModel 型が結合されるため、再利用性が低くなり、テストとプレビューが難しくなります。また、ViewModel インスタンスを管理する明確な SSOT(信頼できる単一の情報源)がなくなります。ViewModel を渡すと、複数のコンポーザブルが ViewModel 関数を呼び出して状態を変更できるようになり、バグのデバッグが難しくなります。
ViewModelと各UIコンポーネントの結合度が上がるためです。ViewModelありきのコンポーネントになってしまっては、再利用がしにくくなります。せっかく部品として小さく作りやすくなったJetpack Composeの利点を消してしまうことになっています。
さらに、画面レベルのコンポーネントはまだしもそこから各子のコンポーネントにViewModelを渡してしまうと、情報源を複数の場所で持つことになり、変更が各地で可能になってしまいます。これはいわゆるSSOT(Single Source of Truth)に反します。
また、そもそも一つのコンポーネントが知る範囲としてViewModelは大きすぎるというのもあると思います。
このように、AAC ViewModelとJetpack Composeの各コンポーネントの境界はしっかりと分ける必要があると感じました。公式によると、子のコンポーザブルにはViewModelではなく代わりにプレーンオブジェクトを渡すべき、とのこと。ちなみにこれが先述の2つ目の状態ホルダー、「UIロジック状態ホルダー」に値します。
AAC ViewModel使うなら・・・
画面レベルのコンポーザブル with ViewModel | 画面が持つ子のコンポーザブル with plain class
上に述べたAAC ViewModelの恩恵を受けるには、注意すべきこともあるようです。
AAC ViewModelの代わりになるものを使うでもいいと考える理由
ところで、私は公式が必ずしもAAC ViewModelを使えと言っているわけではないと感じます。
その根拠も、公式の見解の中にあると思っています。
注: 実際のユースケースで ViewModel のメリットを活用できない場合や、別の方法で行う場合は、ViewModel の役割をプレーンな状態ホルダークラスに移行できます。
そもそも長期にデータを保持する要件がないとか、前の画面に戻ってリロードする仕様は許容できるとか、画面回転に関してもマニフェストファイルで設定できるとか、アプリによって、画面によってViewModelがなくてもいいシーンは意外とありそうです。
データ保持に関してはrememberSavableや、dataレイヤーでキャッシュ機構を設ける手もありそうです。つまりViewModelが無いと絶対に出来ないという話ではなく、何らかの対策を施す余地は十分にあると予想しています。
特に新規のアプリ開発なら、この辺りの実装方針はチームで先に固めておけるような気もします。
また、肥大化したViewModelはAndroid View時代からも見られる気がします。これは、細分化するUIの粒度に対してViewModelの粒度が合っていないところに関係がありそうです。
策としては、各UIに対する状態とロジックを閉じ込めたplain classに切り出すことで、削減出来る部分があると考えます。
例えば、ローディングはあらゆる画面で使われます。このような頻出UIの状態やロジックは、そのViewModel特有の状態やロジックではありません。にもかかわらず各地で、例えばRxRelayならval isLoading: BehaviorRelay<Boolean>
というプロパティをaccept
して更新をかけている所を(少なくとも私のプロジェクトでは)よく見かけます。共通化できそうな部分まで1つのViewModelが一身に引き受けているため、コード量を増やすことになる一因となっていると感じます。
あらゆる画面で参照される状態や、ビジネスロジックの呼び出し部分に関しても同様です(ビジネスロジックに関しては、ユースケースパターンを使えそうですね)。
まとめ
- そもそもViewModelの役割って何だっけ
- 表示と状態・ロジックを切り出す、関心の分離。Androidで言えば、ActviityとFragmentへの依存をなるべく減らすため。
- AAC ViewModel使ってもいいかも
- AAC ViewModelのスコープや、NavigationやHiltとのシナジーが依然としてあるから
- ただしJetpack ComposeとViewModelは疎であるべき
- 使わない手もありかも
- 公式も絶対AAC ViewModel使って!ではなさそう
- ViewModelの肥大化や粒度の設計を見直せる良い機会かも
参考記事
大変参考になりました
(iOSエンジニアの方にはこちらもおすすめさせてください)
(Navigation Compose使ってFragmentがいないプロジェクト見るの快感)