0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ViewModelが画面回転で死なない理由 — ViewModelStoreの内部メカニズムを紐解く

0
Posted at

はじめに

Android開発において、ViewModel が画面回転(構成変更: Configuration Changes)をまたいでデータを保持してくれるのは、今や当たり前の挙動です。

しかし、「Activityは一度破棄されて再生成されているのに、なぜViewModelのインスタンスは同じものを維持できるのか?」 と疑問に思ったことはないでしょうか。

その秘密を握るのが ViewModelStore です。この記事では、普段あまり意識することのない ViewModelStore の内部構造と、インスタンスが維持される仕組みを解説します。


1. ViewModelStore とは?

ViewModelStore は、一言で言えば ViewModel のインスタンスを管理・キャッシュするための保管庫(クラス) です。

内部のソースコードを見ると非常にシンプルで、実体は ViewModel を保持するただの HashMap です。

// 実際のソースコードの簡略化イメージ
public class ViewModelStore {
    private val map = HashMap<String, ViewModel>()

    internal fun put(key: String, viewModel: ViewModel) {
        val oldViewModel = map.put(key, viewModel)
        oldViewModel?.onCleared()
    }

    internal fun get(key: String): ViewModel? {
        return map[key]
    }

    public fun clear() {
        for (vm in map.values) {
            vm.clear() // 内部で onCleared() が呼ばれる
        }
        map.clear()
    }
}

開発者が ViewModel を直接 new せず、ViewModelProvider を経由して取得するのは、この ViewModelStore(HashMap)から既存のインスタンスを検索・再利用するためです。


2. 三種の神器:ViewModelを支える3つのコンポーネント

ViewModelの生存戦略は、以下の3つの要素が連携することで成り立っています。

コンポーネント 役割 主なクラス
ViewModelStoreOwner ViewModelStore を「所有」するインターフェース。 ComponentActivity, Fragment
ViewModelStore ViewModel のインスタンスを格納する「保管庫」。 (本記事の主役)
ViewModelProvider 保管庫からViewModelを「取り出す / なければ生成する」窓口。 開発者がコードで叩くクラス

関係性のイメージ

  1. Activity(Owner)に「あなたの ViewModelStore をください」と頼む。
  2. ViewModelProvider(窓口)が、その ViewModelStore(保管庫)に「MainViewModel はもうある?」と確認する。
  3. あればそれを取り出し、なければFactoryで新しく作って保管庫に格納した上で返す。

3. なぜ画面回転をサバイブできるのか?

ここからが本題です。Activityが描き直される際、なぜ ViewModelStore(HashMap)は一緒に消えないのでしょうか?

NonConfigurationInstances メカニズム

Android OSには、画面回転などの構成変更時に、システムがActivityを破棄しつつも「特定のオブジェクトだけを次の新しいActivityに直接手渡す」バックドア(仕組み)が用意されています。

かつては onRetainCustomNonConfigurationInstance() という古いAPIで実現されていましたが、現在のJetpack(ComponentActivity)の内部では以下のような流れで ViewModelStore がリレーされています。

[古い Activity インスタンス]
       │
       ├─► 画面回転発生!
       │
       ├─► retainNonConfigurationInstances() が呼ばれ、
       │   `ViewModelStore` をシステム(NCI)に預ける
       │
───────┼──────────────────────────────────────────
       ▼
[新しい Activity インスタンス]
       │
       ├─► onCreate() / 復元フェーズ
       │
       └─► getLastNonConfigurationInstance() から
           古い `ViewModelStore` をそのまま引き継ぐ!

Activityという「ガワ」は新しくなりますが、その中身である ViewModelStore(HashMapインスタンス)自体はメモリ上で破棄されずに使い回される ため、中に入っている ViewModel もそのまま生存できるのです。


4. いつ消える?ViewModelの本当の終焉

画面回転では生き残る ViewModelStore ですが、当然寿命はあります。ユーザーがアプリを明示的に閉じたり、finish() が呼ばれたりして 「Activityが本当に死ぬ時」 です。

ComponentActivityFragment は、自身が完全に終了する(Configuration Change以外で破棄される)タイミングを検知すると、ViewModelStore.clear() を呼び出します。

// ComponentActivity の内部実装イメージ
lifecycle.addObserver(LifecycleEventObserver { source, event ->
    if (event == Lifecycle.Event.ON_DESTROY) {
        // 構成変更(画面回転)による破棄ではない場合のみ、完全にクリアする
        if (!isChangingConfigurations) {
            viewModelStore.clear()
        }
    }
})

clear() が呼ばれると、HashMap内のすべての ViewModel に対して順に onCleared() が実行されます。これにより、ViewModel内で実行中だったコルーチン(viewModelScope)のキャンセルや、リソースの解放が安全に行われます。


まとめ:普段の開発でどう活きる?

一般的なアプリ開発では ViewModelStore を直接操作することはほぼありませんが、この仕組みを知っていると以下のような応用・デバッグで役立ちます。

  • カスタムスコープの作成: Navigationコンポーネントの NavBackStackEntryViewModelStoreOwner インターフェースを実装しているため、特定の画面グラフ内だけで共有するViewModelスコープ(by navGraphViewModels)が実現できている。
  • メモリリークの防止: onCleared() が呼ばれるタイミング(=Activityの完全な終了時)を正確に把握することで、非同期処理やリスナーの解除漏れを防ぐ意識が高まる。

次から by viewModels() を書くときは、裏でHashMapが健気にリレーされている様子を思い出してみてください。


参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?