ViewModel が画面回転しても保持される仕組み

この記事は Android Advent Calendar 2018 12/6 の記事です。

  • この記事は androidx をベースに書いてます。
    • compileSdkVersion 28
    • androidx.appcompat:appcompat:1.0.2
  • 簡潔に説明するため、一部のコードを省略しています。

次のコードのように Android Architecture Components の ViewModelProvider を経由して ViewModel のインスタンスを取得しておくと、 HogeActivity が回転した後も、回転前と同じ ViewModel のインスタンスを取得できる仕組みになっています。

class HogeActivity : AppCompatActivity {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d(TAG, "onCreate")

    val activity: FragmentActivity = this
    val factory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory()

    // viewModel は Activity 回転前も後も同じインスタンス
    val viewModel = ViewModelProvider(activity, factory).get(FooViewModel::class.java)
    // 直接インスタンス化しては、 Activity 回転前後で同じインスタンスが使えない
//  val viewModel = FooViewModel()

    Log.d(TAG, "  - Activity :${this.hashCode()}")
    Log.d(TAG, "  - ViewModel:${viewModel.hashCode()}")
  }
}
// Activity が起動した
onCreate
  - Activity :132818886
  - ViewModel:249530701
onStart
onResume

// Activity が回転した
onPause
onStop
onRetainNonConfigurationInstance
onDestroy
onCreate
  - Activity :103312713
  - ViewModel:249530701 <- 同じ ViewModel であることがわかる
onStart
onResume

ViewModel は Activity 回転時のライフサイクルを超えてインスタンスが保持されることが、公式サイトに書いてあります。

https://developer.android.com/topic/libraries/architecture/viewmodel#lifecycle

Activity が回転しても ViewModel が保持されるのはどういう仕組みなのか、紐解いていきたいと思います。

まず、お目当ての ViewModel は ViewModelProvider から取得していることがわかりますね。

val viewModel = ViewModelProvider(activity, factory).get(FooViewModel::class.java)

この ViewModelProvider の中身がどういう実装なのか見ていきます。

ViewModelProvider

ViewModelProvider は薄い実装になっています。

ViewModelProvider.Factory と ViewModelStore をインスタンスフィールドに持ち、

package androidx.lifecycle;

public class ViewModelProvider {

    public interface Factory {
        @NonNull
        <T extends ViewModel> T create(@NonNull Class<T> modelClass);
    }

    private final Factory mFactory;
    private final ViewModelStore mViewModelStore;

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        this.mViewModelStore = store;
    }

    ...
}

ViewModel を返す get() メソッドを持っています。

package androidx.lifecycle;

public class ViewModelProvider {
    ...

    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();

        ...

        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }

    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            //noinspection unchecked
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: log a warning.
            }
        }

        viewModel = mFactory.create(modelClass);
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }

    ...
}

ViewModelProvider はクラス名のとおり、 ViewModel のインスタンスを外部へ提供しています。

その提供の仕組みは単純で、 ViewModelStore から ViewModel を取得して返します。もし取得できなかった場合は ViewModelProvider.Factory に ViewModel を新たに生成してもらい、 ViewModelStore に格納してから返していることがわかります。

つぎに、ViewModelProvider が持っている ViewModelStore も見ていきます。

ViewModelStore

ViewModelStore も ViewModelProvider と同じぐらい薄い実装になっています。

package androidx.lifecycle;

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }
}

get()put() のアクセス修飾子がデフォルトになっているので、アプリから ViewModelStore に対して ViewModel の操作をすることはできないようになっています。実際は ViewModelProvider の get() の中からアクセスされており、 ViewModel の取得は必ず ViewModelProvider の get() を経由する必要があることがわかります。

ここでもう一度、冒頭のコードを見てみましょう。

class HogeActivity : AppCompatActivity {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val activity: FragmentActivity = this
    val factory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory()

    // viewModel は Activity 回転前も後も同じインスタンス
    val viewModel = ViewModelProvider(activity, factory).get(FooViewModel::class.java)
  }
}

ViewModelProvider コンストラクタの第 1 引数に渡しているのは FragmentActivity ですが、実際は ViewModelStoreOwner 型として ViewModelProvider は受け取っています。

その ViewModelStoreOwner の getViewModelStore() を呼び出して取得した ViewModelStore のインスタンスを、 ViewModelProvider は自身の mViewModelStore に代入していますね。

public class ViewModelProvider {

    private final ViewModelStore mViewModelStore;

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        this.mViewModelStore = store;
    }

この ViewModelStoreOwner も薄いインターフェースです。

package androidx.lifecycle;

public interface ViewModelStoreOwner {
    @NonNull
    ViewModelStore getViewModelStore();
}

FragmentActivity がどのように ViewModelStoreOwner#getViewModelStore() を実装しているか見てみましょう。

FragmentActivity implements ViewModelStoreOwner

FragmentActivity が実装している ViewModelStoreOwner#getViewModelStore() は次のようになっています。

package androidx.fragment.app;

public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner ... {

    private ViewModelStore mViewModelStore;

    @NonNull
    @Override
    public ViewModelStore getViewModelStore() {
        ...

        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }

    static final class NonConfigurationInstances {
        Object custom;
        ViewModelStore viewModelStore;
        FragmentManagerNonConfig fragments;
    }

    ...
}

FragmentActivity は ViewModelStore をインスタンスフィールドに持っていて、 getViewModelStore() を呼び出した場合は必ず mViewModelStore に ViewModelStore のインスタンスが代入されます。

さて、 ViewModelStore のインスタンスの取得方法を見ると、 getLastNonConfigurationInstance() から NonConfigurationInstances クラスのインスタンスを取得し、それに格納されている viewModelStore を取得していますね。

この getLastNonConfigurationInstance() は何者なんでしょうか?

Activity#getLastNonConfigurationInstance()

getLastNonConfigurationInstance() の実装は次のようになっています。

package android.app;

public class Activity extends ContextThemeWrapper implements ... {

    /* package */ NonConfigurationInstances mLastNonConfigurationInstances;

    @Nullable
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }

https://developer.android.com/reference/android/app/Activity.html#getLastNonConfigurationInstance()

Retrieve the non-configuration instance data that was previously returned by onRetainNonConfigurationInstance(). This will be available from the initial onCreate(Bundle) and onStart() calls to the new instance, allowing you to extract any useful dynamic state from the previous instance.

ドキュメントを見るに、 onRetainNonConfigurationInstance() が返していたインスタンスgetLastNonConfigurationInstance() から取得できるようです。

ここでいう "instance" はおそらく Activity のことでしょうか、 Activity 回転前で onRetainNonConfigurationInstance() が返していたインスタンスを、回転後に getLastNonConfigurationInstance() を呼び出すことで、そのインスタンスを取得できるようですね。

このあたりで察しが付けられそうですが、 Activity が回転しても ViewModel が保持される仕組みは、この onRetainNonConfigurationInstance() がカギを握っています。

Activity#onRetainNonConfigurationInstance()

以下は、Activity が回転したときのライフサイクルをログに出力したものです。

// Activity が起動した
onCreate
  - Activity :132818886
  - ViewModel:249530701
onStart
onResume

// Activity が回転した
onPause
onStop
onRetainNonConfigurationInstance
onDestroy
onCreate
  - Activity :103312713
  - ViewModel:249530701 <- 同じ ViewModel であることがわかる
onStart
onResume

Activity が回転したときのライフサイクルの中では onRetainNonConfigurationInstance() というメソッドも呼ばれます。このメソッドは onStop()onDestroy() の間で呼ばれ、 Activity が回転して再生成されることがわかっているときに任意のオブジェクトを返しておくことで、再生成後に getLastNonConfigurationInstance() から任意のオブジェクトを取得することができます。

https://developer.android.com/reference/android/app/Activity#onRetainNonConfigurationInstance()

package android.app;

public class Activity extends ContextThemeWrapper implements ... {

    public Object onRetainNonConfigurationInstance() {
        return null;
    }

    ...
}

この onRetainNonConfigurationInstance() は Activity に定義されていますが、 FragmentActivity がオーバーライドしています。

package androidx.fragment.app;

public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner, ... {

    @Override
    public final Object onRetainNonConfigurationInstance() {
        Object custom = onRetainCustomNonConfigurationInstance();

        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

        if (fragments == null && mViewModelStore == null && custom == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = mViewModelStore;
        nci.fragments = fragments;
        return nci;
    }

    static final class NonConfigurationInstances {
        Object custom;
        ViewModelStore viewModelStore;
        FragmentManagerNonConfig fragments;
    }

    ...
}

FragmentActivity の onRetainNonConfigurationInstance() では NonConfigurationInstances クラスのインスタンスを返しています。そのインスタンスの中には ViewModelStore のインスタンスを格納していますね。

そうです、つまり、 FragmentActivity が回転した後に getLastNonConfigurationInstance() を呼び出すと、回転前に生成された ViewModelStore のインスタンスが格納された NonConfigurationInstances クラスのインスタンスを取得できます。

実際に FragmentActivity#onCreate() のコードを見ると...

package androidx.fragment.app;

public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner ... {

    private ViewModelStore mViewModelStore;

    @SuppressWarnings("deprecation")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);

        super.onCreate(savedInstanceState);

        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null && nc.viewModelStore != null && mViewModelStore == null) {
            mViewModelStore = nc.viewModelStore;
        }
        ...
    }
}

getLastNonConfigurationInstance() から NonConfigurationInstances クラスのインスタンスを取得し、 ViewModelStore のインスタンスmViewModelStore に代入していますね。

ViewModel が画面回転しても保持される仕組みとは

ViewModelProvider を経由して ViewModel のインスタンスを取得しておくと、 Activity が回転した後も、回転前と同じ ViewModel のインスタンスを取得できる仕組みになっています。

class HogeActivity : AppCompatActivity {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val activity: FragmentActivity = this
    val factory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory()

    // viewModel は Activity 回転前も後も同じインスタンス
    val viewModel = ViewModelProvider(activity, factory).get(FooViewModel::class.java)
  }
}

このコードをもとに、 HogeActivity の回転前後の軌跡をまとめると...

回転前

  • FragmentActivity#onCreate() が呼ばれる
    • この時点ではまだ FragmentActivity の mViewModelStore は null
  • HogeActivity#onCreate() が呼ばれる
    • ViewModelProvider インスタンスを生成する
      • この時点で、 FragmentActivity#getViewModelStore() が呼ばれるため FragmentActivity の mViewModelStoreインスタンスが代入されている
    • ViewModelProvider#get() から ViewModel のインスタンスを取得できる
    • この ViewModel のインスタンスは、 FragmentActivity が持つ mViewModelStore に格納されている

回転時

  • FragmentActivity#onRetainNonConfigurationInstance() が呼ばれる
    • FragmentActivity が持つ mViewModelStore を格納した NonConfigurationInstances クラスのインスタンスを返している

回転後

  • FragmentActivity#onCreate() が呼ばれる
    • Activity#getLastNonConfigurationInstance() から、NonConfigurationInstances クラスのインスタンスを取得している
    • NonConfigurationInstances クラスのインスタンスに格納されている、回転前の FragmentActivity が持っていた mViewModelStore を、 回転後の FragmentActivity は自身の mViewModelStore に代入している
  • HogeActivity#onCreate() が呼ばれる

ということになります。