はじめに
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を「取り出す / なければ生成する」窓口。 | 開発者がコードで叩くクラス |
関係性のイメージ
-
Activity(Owner)に「あなたのViewModelStoreをください」と頼む。 -
ViewModelProvider(窓口)が、そのViewModelStore(保管庫)に「MainViewModelはもうある?」と確認する。 - あればそれを取り出し、なければ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が本当に死ぬ時」 です。
ComponentActivity や Fragment は、自身が完全に終了する(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コンポーネントの
NavBackStackEntryもViewModelStoreOwnerインターフェースを実装しているため、特定の画面グラフ内だけで共有するViewModelスコープ(by navGraphViewModels)が実現できている。 -
メモリリークの防止:
onCleared()が呼ばれるタイミング(=Activityの完全な終了時)を正確に把握することで、非同期処理やリスナーの解除漏れを防ぐ意識が高まる。
次から by viewModels() を書くときは、裏でHashMapが健気にリレーされている様子を思い出してみてください。
参考リンク