(Android) Architecture ComponentsのViewModelは如何にしてRotationを生き残るか

TL;DL;

retainInstance = trueなFragmentにキャッシュされているので、Activity/Fragmentが本当に殺されるまで生き残ることができる。


Google I/O 2017で、Architecture Components という新しいライブラリ群が発表されました。

LiveData, ViewModel, LifecycleObserver, LifecycleOwner, Roomといったこれらのライブラリは、開発者がより強固で、テスタブルで、かつメンテナンス性が高いアプリケーションを作るための手助けとなるべく作られています。

今回はその中でもViewModelについて少し調べてみました。

What is ViewModel?

詳しい説明はViewModelのリファレンスに譲りますが、簡単に言うと「Activity/Fragmentのローテーション等による再生成をこえて状態を保持するためのコンポーネント」です。

今までActivity/Fragmentのメンバ変数に保存していたような値や非同期処理をViewModelに書いておけば、Activity/Fragmentが再生成されたとしても値や非同期処理の状態が維持されてそのまま使えます。

onSavedInstanceStateでいちいちBundleに詰め込んだりしなくてすみます。 私自身はあまり使ったことがありませんが、AsyncTask等をいちいち書く必要がなくなります。

でも、どうやって?

How does ViewModel retain itself?

※これはGoogle I/O 2017で発表された1.0.0-alpha1時点での話です

ViewModelは下記のような感じで取得します。使う側のActivity/Fragmentではキャッシュから取得するとか新規作成するとか、そういうことを意識する必要はありません。

java
FooViewModel viewModel = ViewModelProviders.of(fragment).get(FooViewModel.class);

ViewModelProviders#ofにはActivityかFragmentを渡すことができます。一つ注意することがあって、Fragmentを渡すときはfragment.getActivity() != nullでなければならず、detachされているFragmentでは使うことができません。

ViewModelProviders#ofはActivity/Fragmentのみを引数に取るものと、Activity/Fragmentに加えてViewModelのファクトリクラスを引数に取るものがあります。

ファクトリクラスを渡さない場合は引数なしのコンストラクタを呼び出してViewModelをインスタンス化するようです。

また、ViewModelの代わりにAndroidViewModelというinterfaceを実装すると引数がApplicationのコンストラクタを使う模様です。

Dagger等のDIライブラリを使う場合はファクトリクラスを使うことになりそうです。

さて、ViewModelProviderはこんな感じでインスタンス化されています。

java
// ViewModelProviders.java
public static ViewModelProvider of(@NonNull Fragment fragment) {
    FragmentActivity activity = fragment.getActivity();
    if (activity != null) {
        throw new IllegalArgumentException("Can't create ViewModelProvider for detached fragment");
    } else {
        initializeFactoryIfNeeded(activity.getApplication());
        return new ViewModelProvider(ViewModelStores.of(fragment), sDefaultFactory);
    }
}

呼び出されているコンストラクタのシグネチャはこんな具合です。

ViewModelProvider(ViewModelStore store, ViewModelProvider.Factory factory)

ViewModelStoreとかいかにもあやしい名前のクラスが引数にあるので詳しく見てみましょう。 ViewModelStores#ofの中を見てみるとこんな感じ

java
public static ViewModelStore of(Fragment fragment) {
    return HolderFragment.holderFragmentFor(fragment).getViewModelStore();
}

HolderFragmentなるものが出てきました。

コードを見てみると、コンストラクタでsetRetainInstance(true);を呼んでいます。retainInstance = trueにすると親Activity/Fragmentが再生成されても対象のFragmentは生き残るようになるので、この仕組みを使ってViewModelをActivity/fragmentの再生成後も使えるようにしているようです。

ViewModelStoreはこのHolderFragmentで管理されています。

ViewModelStoreの実態はHashMap<String, ViewModel>です。ここで基本的にはViewModelのクラス名をキーとしてViewModelのインスタンスを管理しています。

HolderFragmentはActivity/Fragment毎に作られるので、一つのViewModelStoreが管理するのは自HolderFragmentの直接の親Activity/FragmentのViewModelだけです。

クラス名がキーなので同一Activity内、同一Fragment内で同じViewModelを複数使うことはできなそうな感じもしましたが、よくよく見ると外部からキーを指定できるgetメソッドのオーバーロードも用意されていたので一応そういうユースケースも考慮されていそうです。

ViewModelの保存周り、ホントはもうちょっとゴニョゴニョしてるんだけど概要としてはこんな感じです。とてもわかりやすいコードなのでぜひ一読してみてください。

終わりに

ここ一年くらい触ってるyet another FragmentのConductorがController(Fragmentのようなもの)をキャッシュするのにやっぱりretainInstance = trueなFragmentを使ってて、Architecture Componentsの発表を聞いたときに同じようなことやってるのかなーって思ってたら案の定でちょっとニンマリしてしまった。

特に黒魔術してるわけでもなく、既存の仕組みをうまく使ってるだけなのでこの仕組み自体が黒歴史になることはなさそう。

ViewModelはActivity/Fragmentへの参照を持つことは推奨されてないので必然的にユニットテストしやすいコードになっていきそう。

とはいえViewModelだけで完全にテスタブルになるわけではないし、ViewModelはActivity/Fragmentの再生成をこえた状態のキャッシュに一つの道を示しただけなので、これですべてが解決するわけではない。

今までの知見と組み合わせつつ、幸せになれるコードを書けるようやっていきましょう、ということでざっくりとしたコードリーディングを終わります。