ViewModelでライフサイクルを超えたデータの引き継ぎ

投稿日:  更新日:

Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。

その中で紹介されているコンポーネントの1つが「ViewModel」です。

ViewModelについて、まとめました。

スポンサーリンク

ViewModelとは

Activityはアプリケーションコンポーネント(アプリの部品)の1つで、主な役割は一枚の画面表示をすることです。

Activityは、画面表示の内容やユーザが操作した内容など、UI(User Interface)関連のデータを変数(プロパティ)の形で持ちます。そして、データを常に更新しながら、データから次の画面を新たに作って表示します。

端末の状態を変化させる動作(例:端末を縦⇒横へ回転)を行うと、アンドロイドシステムに構成の変更が起こります。システムは、実行していたActivityを終了し、新たな構成でActivityを起動します。

Activity中で宣言した変数の有効範囲はそのインスタンスの中だけです。よって、構成の変更後のActivityへUI関連のデータの引き継は行われません。UI関連のデータは初期値に戻り、Activityは初期画面で起動されます。

ViewModelの概要

ここで問題なのは、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を使って格納する箱です。

ViewModelStoreの構成

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( )を使って取り出します。

ただし、構成の変更が発生した場合のみ有効な機能です。

onRetainNonConfigurationInstanceの機能

この機能を使って、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を格納しています。

NonConfigurationInstancesの構成

つまり、ViewModelStoreはNCIにパッキングされて引き継がれます。

スポンサーリンク

関連記事:

Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。 有効そうだけど、実態がよくわからないので、いろいろ調べて理解した内容をまとめました。 ...
Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。 その中で紹介されているコンポーネントの1つが「Lifecycle」です。 Lifecycleについて、まとめました。 ...
Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。 その中で紹介されているコンポーネントの1つが「LiveData」です。 LiveDataについて、まとめました。 ...
Androidの開発スピードが速過ぎます。個人で習得を進めている私には、追い付いて行けません。 これが会社などであれば、グループ内のメンバーで分担して習得し、後に共有、などといった対応が可能でしょう。組織の強みですね! 先日も、久しぶりにAndroid Studioで新規プロジェクトを作成したら、Jetpack Composeのプロジェクトになってました。「はて?、これはどうすれば良いのだ?」と、頭の中は疑問符だらけで、プログラミングが先へ進めませんでした。 Jetpackの存在は知りながら、“使わなくてもアプリは作れる”と、学習は後回していたからです。 プロジェクトのひな型にJetpack Composeが採用されたならば、今後はJetpackの利用が開発の主軸になっていくのでしょう。 遅れ馳せならが、Jetpackの重要性を知った次第です。 ここで本腰をいれて習得しないと、さらに後方へ置いて行かれそうです。 という訳で、Jetpackの学習を始めました。 今回はJetpackそのものについてまとめます。 ※環境:Android Studio Flamingo | 2022.2.1 ...
スポンサーリンク