本記事を書く経緯
ViewModelをよくこんな感じに使っていましたが
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
}
}
これはある程度の「魔術」だと思って、viewModelsの中で何をやっているのか、なんでActivityのライフサイクルを超えて生存できるのかを気にしていませんでしたが、Activity間で同じViewModelを共有したい要件に出会ったので、本格的にViewModel周りを調査してみました。
ViewModelの生成
まずAndroid KTXを使わないでViewModelを利用する原始的な書き方を見てみます。
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
// (Deprecated) viewModel = ViewModelProviders.of(this).get(MainViewModel::class)
viewModel = ViewModelProvider(this).get(MainViewModel::class)
}
}
Android KTXが出る前まではたぶん一番こういう書き方がよく見られます。
ただ、この書き方はデフォルトで用意されているViewModelProvider.Factoryを使用する前提になっているし、登場人物はまだ出揃っていないので、解説には不向きです。もうちょっとカスタマイズした書き方を見てみます。
// 2. Activityは元からViewModelStoreOwnerを実装しています
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
// 1. ViewModelProvider(owner: ViewModelStoreOwner, factory: ViewModelProvider.Factory)
val provider = ViewModelProvider(this, MainViewModelFactory())
viewModel = provider.get(MainViewModel::class)
}
}
// 3. 実際にViewModelを生成するFactoryクラス
class MainViewModelFactory : ViewModelProvider.Factory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// ViewModelを手動生成する
return MainViewModel() as T
}
}
class MainViewModel : ViewModel() {
...
}
上記のViewModelProvider、ViewModelStoreOwner、ViewModelProvider.FactoryはViewModelの取得に関わる主要人物です。一個ずつ見ていきます。
ViewModelStoreOwner
public interface ViewModelStoreOwner {
/**
* Returns owned {@link ViewModelStore}
*
* @return a {@code ViewModelStore}
*/
@NonNull
ViewModelStore getViewModelStore();
}
ViewModelStoreOwnerはViewModelStoreを返す役割を持っています。ViewModelの保存に関わっています。
ViewModelStoreは自体一種のHashMapで、ViewModelを保存/返す役割を持っています。
ViewModelStoreOwnerが返すViewModelStoreは毎回違うのか、それとも同じViewModelStoreを返すかはViewModelStoreOwnerの実装次第です。
ActivityはこのViewModelStoreOwnerを実装しています。回転などで再生成されたActivityでも一律前と同じViewModelStoreのインスタンスを返すように実装されているので、Activityのライフサイクルを超えてViewModelは生存できました。
ViewModelProvider.Factory
public interface Factory {
/**
* Creates a new instance of the given {@code Class}.
* <p>
*
* @param modelClass a {@code Class} whose instance is requested
* @param <T> The type parameter for the ViewModel.
* @return a newly created ViewModel
*/
@NonNull
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}
ViewModelProvider.FactoryはViewModelの生成に関わっています。
Factoryを使わなくても例えば普通にMainViewModel()を呼んだら生成できますが、Factoryを通すことで 生成する/しない ・ 生成のタイミング をViewModelProviderに移譲します。
ViewModelProvider
public class ViewModelProvider {
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
// mViewModelStoreに取得しようとしているViewModelがあればそのまま返す
// なければmFactoryを使ってViewModelを生成した上で、mViewModelStoreに入れてあとに返す
}
}
ViewModelProviderは大体デザインパターンでいうとBuilderパターンのDirectorの役割です。FactoryとViewModelStoreOwnerを制御してViewModelを返します。
Factoryを使ってViewModelを生成し、ViewModelStoreに入れる作業をするし、ViewModelがすでにViewModelStoreに入っていたら生成しないで、直接ViewModelを返します。
ActivityのViewModelStoreOwnerの実装
回転などで再生成されたActivityでも一律前と同じViewModelStoreのインスタンスを返すことで、Activityのライフサイクルを超えてViewModelは生存できました。
ではActivityはどうやって同じViewModelStoreを返すのか、これはComponentActivity1内のgetViewModelStoreの実装を見ればわかります。
結論を先にいうと、getLastNonConfigurationInstanceとonRetainNonConfigurationInstanceを使って実現してました。
class ComponentActivity extends androidx.core.app.ComponentActivity implements ViewModelStoreOwner, ... {
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
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;
}
}
mViewModelStoreがまだ初期化されていなければ、getLastNonConfigurationInstanceからViewModelStoreを取得します。
このgetLastNonConfigurationInstanceはあまり馴染みがないので、調べてみました。どうもViewModelより以前からある仕組みです。
public Object getLastNonConfigurationInstance()
public Object onRetainNonConfigurationInstance()
Activityがdestroyされる前にonRetainNonConfigurationInstanceは呼ばれます。この関数をオーバーライドして任意のオブジェクトを返せば、次にActivityが再生成されたときにgetLastNonConfigurationInstanceから完全同一オブジェクトを取得できます(そのオブジェクトは死なないです)。
ComponentActivityはこの仕組を利用して、Activityがdestroyされる前にViewModelStoreをonRetainNonConfigurationInstanceで一旦退避させて、Activityが再生成されてgetViewModelStoreが呼ばれたときに同じオブジェクトを返すようにしました。
このgetLastNonConfigurationInstanceとonRetainNonConfigurationInstanceは、今では使われない機能らしいですが、昔はそれなりに活躍していたらしいです。だが今はComponentActivity内ではonRetainNonConfigurationInstanceをfinalにしているので、オーバーライドさせてもらえないです。
(代わりに名前が似ているド同等機能のonRetainCustomNonConfigurationInstanceとgetLastCustomNonConfigurationInstanceがありますが、この2つはfinalになってないのでオーバーライドできますが、Deprecatedな関数なので、あえて使う必要がないでしょう。)
結論
ViewModelの生成はFactoryの責務で、ViewModelの保存・生存期間はViewModelStoreOwner(ViewModelStore)の責務です。
Activity間でViewModelを共有したければ、共通のViewModelStoreを持てばいいです。
そのViewModelStoreはせめてActivityより長生きしないとだめなので、Activityより長いライフサイクルを持つところにそのViewModelStoreを置くしかないです。
つまり手取り早くApplicationに置くことになりそうです。(もしくはDaggerでViewModelStoreのシングルトンを注入します)