musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

ViewModelのスコープを意識する

ViewModelは画面の状態を保持する役割があります。
今回は画面の状態をいつまで保持するかについての話。

ViewModelの概要

ViewModelは紐づけたFragmentが破棄されるまで生き続けます。
この生存期間のことをスコープといいます。
ViewModelを生成する時は、適切なスコープに設定しないと意図しない動作となりハマります。


例えば

musicLineでは自分の曲をアルバムにまとめる機能があります。
アルバムを作成する際、アルバムに収録する曲を複数曲選択します。この時にコミュニティに投稿した自分の曲が一覧で表示されます。スコープを間違えると曲一覧表示する場面でよくない挙動になります。

アルバム作成画面で曲を選択する
スコープが適切な場合

想定する挙動

アルバム作成画面で「+マイソングから曲を追加」ボタンを押したときに、自分の曲リストをサーバーに問い合わせて曲一覧画面に表示します。サーバーで問い合わせた曲リストは曲一覧画面のViewModelにキャッシュし、再度開いた時にキャッシュした曲リストを表示します。

アルバム作成画面
曲一覧画面

おかしい挙動

ViewModelで曲リストをキャッシュしているため、曲一覧画面を最初に開くときのみサーバーに問い合わせれば、その後問い合わせる必要はないはずですが。。
ViewModelのスコープを間違えると、曲一覧画面を開くたびにサーバーに問い合わせることになります。これは、曲一覧画面のFragmentが破棄された時にViewModelも一緒に破棄されるからです。
この状態だとアルバム曲を10曲選択するとしたら、10回サーバーに曲リストを問い合わせるのでとても無駄です。また、Viewの状態を保持できていないため、曲一覧画面を開くたびにスクロールが一番上に戻ってしまうので使い勝手も悪いです。

アルバム作成画面で曲を選択する
スコープが不適切な場合
画面を開くたびに曲を問い合わせる


スコープを設定する基準

現在のFragmentがスコープ

今回の場合は、曲一覧画面が破棄されても状態を保持したいので、ViewModelのスコープを親のアルバム作成画面に設定してスコープを広くすることで解決します。

親のFragmentがスコープ


なお、スコープが広ければ良いわけではなく、適切なスコープに設定しないと予期せぬ不具合等につながる可能性もあります。
たとえば、スコープを一番上のユーザーページに設定する場合を考えてみます。

一番上のActivityがスコープ

スコープが広がることで、スコープ内のFragmentからViewModelのインスタンスを共有できるという利点がありますが、ユーザーページが閉じられるまでキャッシュが残り続けるという欠点があります。
具体的には、ユーザーページでは自分の曲を削除することができますが、自分の曲を削除した時に不具合が起こります。曲を削除してもViewModelのキャッシュは残っているため、削除したはずの曲が曲一覧に表示されます。

スコープ設定の基準としては最初から広いスコープに設定するのではなく、

  • データをキャッシュしたい場合
  • 親や他のFragment間でデータを共有したい場合

にスコープを広げる検討をしよう。

なるべく小さいスコープを心がけて状況に応じてスコープを広げることが大事です。


ViewModelの生成方法

ViewModelのインスタンス生成方法を3種類紹介します。
詳細はドキュメントを参照。
developer.android.com

1.現在のFragmentのみのスコープ
val myViewModel1 by viewModels<MyViewModel>()

Fragmentが破棄される時にViewModelも破棄されます。

2.親のFragmentがスコープ
val myViewModel2 by viewModels<MyViewModel>({requireParentFragment()})

親のFragmentが破棄される時にViewModelも破棄されます。
また、既に親のFragmentや他のFragmentでViewModelが作成されている時は、そのインスタンスを使います。

3.Activityがスコープ
val myViewModel3 by activityViewModels<MyViewModel>()

Activityが破棄される時にViewModelも破棄されます。こちらも既にViewModelが作成されている時は、そのインスタンスを使います。


プロパティ監視のスコープ(おまけ)

ViewModelのスコープといえば、プロパティ監視する時のスコープも気を付けるべき。
observeの第一引数でスコープ設定できるが、FragmentではthisじゃなくてviewLifecycleOwnerを使うようにしよう。
そうじゃないと、重複してプロパティ監視が登録されることになります。