Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。
その中で紹介されているコンポーネントの1つが「ViewModel」です。
ViewModelについて、まとめました。
ViewModelとは
Activityはアプリケーションコンポーネント(アプリの部品)の1つで、主な役割は一枚の画面表示をすることです。
Activityは、画面表示の内容やユーザが操作した内容など、UI(User Interface)関連のデータを変数(プロパティ)の形で持ちます。そして、データを常に更新しながら、データから次の画面を新たに作って表示します。
端末の状態を変化させる動作(例:端末を縦⇒横へ回転)を行うと、アンドロイドシステムに構成の変更が起こります。システムは、実行していたActivityを終了し、新たな構成でActivityを起動します。
Activity中で宣言した変数の有効範囲はそのインスタンスの中だけです。よって、構成の変更後のActivityへUI関連のデータの引き継は行われません。UI関連のデータは初期値に戻り、Activityは初期画面で起動されます。
ここで問題なのは、Activityが初期画面で起動される事です。
ユーザは構成の変更の前後でUI関連のデータが同じ状態になる(表示の内容が保存される)ことを期待します。これまでの経過を無駄にしたくないからです。
ViewMoldeはライフサイクルを超えて保持されるオブジェクトを作り、構成の変更の前後で同じオブジェクトのインスタンスが取得できる仕組みです。
このオブジェクト内にUI関連のデータを格納しておけば、構成の変更後のActivityへ引き継ぐことができます。
これは、ライフサイクルまたはActivityからUI関連のデータを分離する結果になります。
ViewModelの実装
ViewModelクラスを継承したクラスを作り、その中にUI関連のデータを格納します。
ViewModelが継承されていればよく、削除時に実行されるonCleared( )の実装は任意です。
class MainViewModel : ViewModel() { private val counter = Counter(0) // UIデータ var fourFigure: String = "0000" // UIデータ private set // クリックに従ってCounterを操作 fun onClick(resID: Int) { when(resID) { R.id.btnCount -> { counter.countUp() } R.id.btnClear -> { counter.clear() } else -> {} } fourFigure = "%04d".format(counter.value) } override fun onCleared() { super.onCleared() // ここに // ViewModelの削除時に実行する処理を書く } }
上記で作成したViewModelクラスのインスタンスをViewModelProvider経由で取得すると、次のことが行われます。
・メモリーにインスタンスなし ⇒ 新規にViewModelのインスタンス作成
・メモリーにインスタンスあり ⇒ 既存のViewModelのインスタンス取得
・ライフサイクルを超えて保持されるオブジェクトを構築
※メモリー:ViewModelStoreクラスのHashMapのこと
class MainActivity : AppCompatActivity() { // ViewModelのインスタンス作成、またはインスタンス取得 private val viewModel: MainViewModel by lazy { ViewModelProvider( this, ViewModelProvider.NewInstanceFactory() ).get(MainViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { ... // Activityの起動で初期値を取得し、表示を更新 _txtCounter.text = viewModel.fourFigure _btnCount.setOnClickListener { // CountUpボタンのクリックでカウンタを+1したら、表示を更新 viewModel.onClick(it.id) _txtCounter.text = viewModel.fourFigure } _btnClear.setOnClickListener { // Clearボタンのクリックでカウンタを0にしたら、表示を更新 viewModel.onClick(it.id) _txtCounter.text = viewModel.fourFigure } } }
class Counter(value: Int = 0) { var value: Int = value private set(value) { field = value } fun countUp() { value = if(value == 9999) 0 else ++value } fun clear() { value = 0 } }
得られたViewModelインスタンスはライフサイクルを超えて保持されるオブジェクトです。
このViewModelインスタンス内のUI関連のデータを使っていれば、構成の変更の前後でデータが同じ状態になり、初期画面へ戻ることは起こりません。
【注意点】
オブジェクトの寿命は「ViewModel>Activity」になります。よって、ViewModelからActivityを参照する行為はメモリーリークの原因になります。そのような参照を行う場合は対策が必要です。
補足1:データを引き継ぐ仕組み
データを引き継ぐ仕組みはActivity#onRetainNonConfigurationInstanceの機能を使っています。
※現在はシステムレベルの機能であり、アプリ開発者には非推奨です。
主役はViewModelStoreクラスですので、ViewModelStoreを中心に説明します。
ViewModelインスタンスの取得
ViewModelStoreはViewModelをHashMapを使って格納する箱です。
ComponentActivityのフィールド(mViewModelStore)に代入されています。
ViewModelProvider#get( )により、ViewModelインスタンスはViewModelStoreから取得されます。また、ViewModelStoreがViewModelインスタンスを持っていない時はFactoryクラスによって新規作成されます。
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; } ... @NonNull @MainThread public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) { ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { // インスタンスを持っている if (mFactory instanceof OnRequeryFactory) { ((OnRequeryFactory) mFactory).onRequery(viewModel); } return (T) viewModel; } else { ... } if (mFactory instanceof KeyedFactory) { // インスタンスを持っていない viewModel = ((KeyedFactory) mFactory).create(key, modelClass); } else { viewModel = mFactory.create(modelClass); } mViewModelStore.put(key, viewModel); return (T) viewModel; } ... }
ライフサイクルを超えて引き継ぐ
Activity#onRetainNonConfigurationInstance( )は戻り値で返したオブジェクトを、次に起動されるActivityに渡します。起動されたActivity側でActivity#getLastNonConfigurationInstance( )を使って取り出します。
ただし、構成の変更が発生した場合のみ有効な機能です。
この機能を使って、NCI(NonConfigurationInstances)インスタンスが引き継がれます。その記述がComponentActivityにあります。
public class ComponentActivity extends ... , ViewModelStoreOwner, ... { ... private ViewModelStore mViewModelStore; ... public ComponentActivity() { ... getLifecycle().addObserver(new LifecycleEventObserver() { @Override public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { ensureViewModelStore(); getLifecycle().removeObserver(this); } }); ... } ... void ensureViewModelStore() { if (mViewModelStore == null) { NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { // Restore the ViewModelStore from NonConfigurationInstances mViewModelStore = nc.viewModelStore; } if (mViewModelStore == null) { mViewModelStore = new ViewModelStore(); } } } ... @Override @Nullable @SuppressWarnings("deprecation") public final Object onRetainNonConfigurationInstance() { // Maintain backward compatibility. Object custom = onRetainCustomNonConfigurationInstance(); ViewModelStore viewModelStore = mViewModelStore; ... NonConfigurationInstances nci = new NonConfigurationInstances(); nci.custom = custom; nci.viewModelStore = viewModelStore; return nci; } ... }
NCIはデータクラスです。内部にViewModelStoreを格納しています。
つまり、ViewModelStoreはNCIにパッキングされて引き継がれます。
関連記事: