🌟

Androidにおける ViewModel のライフサイクル

に公開
2026/03/01

はじめに

Jetpack Compose で ViewModel を使うとき、「この ViewModel はいつ破棄されるんだろう?」と疑問に思ったことはないでしょうか。

ViewModel のライフサイクルは、どの ViewModelStoreOwner にスコープするかによって大きく変わります。スコープを誤ると、意図せず画面間で状態が共有されたり、逆にデータが早期に破棄されてしまうといった問題につながります。

本記事では、ViewModelStoreOwner の仕組みを起点に、AOSPのソースコードを交えながら解説します。

前提条件

本記事はシングルアクティビティで Jetpack Compose・Kotlin を利用した構成を前提とします。
説明で利用しているリポジトリはこちらです。3つのスコープをそれぞれブランチで表現しています。
https://github.com/hir05o1/android-viewmodel-lifecycle

ViewModelStoreOwner とは

Jetpack が提供する、ViewModel を保持・管理するためのインターフェース。

  • ViewModel のライフサイクルを管理し、構成変更時でも ViewModel を保持する
  • ViewModelProvider を通じて ViewModel を取得する際の基盤となる

https://github.com/androidx/androidx/blob/c1a767ea375017eb31078308d4c5cb760ee7b4a0/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ViewModelStoreOwner.kt#L18-L29

ViewModelStoreOwner を実装している主なクラスは以下の通り。

  • ComponentActivity(AppCompatActivity は ComponentActivity を継承)
  • Fragment
  • NavBackStackEntry

スコープによってライフサイクルが変わる

ViewModel は ViewModelStoreOwner のライフサイクルに紐付き、ViewModelStoreOwner が完全に削除されるまでメモリ内に残る。

ViewModelStoreOwner の実装は一つではないため、どの実装をスコープとするかによってライフサイクルが変わる。

Activity-scoped ViewModel

ComponentActivity の ViewModelStore に紐づく ViewModel。ViewModelStoreOwner は ComponentActivity となる。

特徴

項目 内容
生存期間 Activity.onCreate() 〜 Activity.onDestroy()
破棄タイミング isChangingConfiguration == false のとき(バックボタンで Activity が終了するとき)
共有範囲 同じ Activity インスタンス内のすべての画面(Single Activity 構成ではアプリ全体)

基本的な使い方

import androidx.activity.viewModels

class MyActivity : AppCompatActivity() {
    // The ViewModel is scoped to `this` Activity
    val viewModel: MyViewModel by viewModels()
}

https://github.com/hir05o1/android-viewmodel-lifecycle/blob/d687729b158684412b4f1a9f5ed4157809589ed2/app/src/main/java/dev/hir05o1/viewmodel_lifecycle/MainActivity.kt#L14-L26

なぜ画面回転(構成変更)で破棄されないのか

ComponentActivity の初期化時に ON_DESTROY イベントを監視し、isChangingConfigurations が false の場合のみ ViewModelStore を破棄しているため。

https://github.com/androidx/androidx/blob/cdbbd60ecd26d31681fdb0ebdaa2902fa56fbeb9/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt#L278-L291

ON_DESTROY が呼ばれるケースは以下の2種類で、ViewModel の扱いが異なる。

ケース 具体例 ViewModel
構成変更(isChangingConfigurations = true) 画面回転・言語変更・ダークモード切り替え 破棄されない
通常の破棄(isFinishing = true) バックボタンでの終了・タスクキル 破棄される

ViewModelStoreOwner は NavBackStackEntry(各 Destination)となる。

特徴

項目 内容
生存期間 navigate() 〜 popBackStack()
破棄タイミング その画面がバックスタックから消えたとき
共有範囲 その画面のみ

基本的な使い方

// MyApp.kt
@Composable
fun MyApp() {
    val navController = rememberNavController()

    NavHost(navController, startDestination = "home") {
        composable("home") { HomeScreen() }
        composable("detail/{id}") { DetailScreen() }
    }
}

// DetailScreen.kt
@Composable
fun DetailScreen() {
    // viewModel() と書くだけで自動的に Navigation-scoped になる
    val viewModel: DetailViewModel = viewModel()
}

https://github.com/hir05o1/android-viewmodel-lifecycle/blob/beabcad534143d98ff1871229fecc1c6028f6fe7/app/src/main/java/dev/hir05o1/viewmodel_lifecycle/NavGraph.kt#L25-L38

なぜ自動的に Navigation-scoped になるのか

viewModel() が内部で LocalViewModelStoreOwner.current を参照しているためで、以下の流れで設定される。

1. NavHost が ViewModelStoreOwner を設定する

https://github.com/androidx/androidx/blob/cdbbd60ecd26d31681fdb0ebdaa2902fa56fbeb9/navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/NavHost.kt#L813-L816

https://github.com/androidx/androidx/blob/cdbbd60ecd26d31681fdb0ebdaa2902fa56fbeb9/navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/NavHost.kt#L818

2. 各画面に NavBackStackEntry を提供する

https://github.com/androidx/androidx/blob/cdbbd60ecd26d31681fdb0ebdaa2902fa56fbeb9/navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/NavHost.kt#L1047-L1055

LocalOwnersProvider の中で NavBackStackEntry が各画面の ViewModelStoreOwner と LifecycleOwner として設定される。

3. viewModel() が LocalViewModelStoreOwner を参照する

https://github.com/androidx/androidx/blob/cdbbd60ecd26d31681fdb0ebdaa2902fa56fbeb9/lifecycle/lifecycle-viewmodel-compose/src/commonMain/kotlin/androidx/lifecycle/viewmodel/compose/ViewModel.kt#L33-L68

この仕組みにより、NavHost 内の Composable で viewModel() を呼ぶと、自動的に NavBackStackEntry がスコープとして選ばれる。


Compose Navigation において、特定の画面(Destination)ではなく、複数の画面を束ねたネストされたナビゲーショングラフに対して ViewModel をスコープする手法。

特徴

項目 内容
生存期間 対象の NavGraph(例:login_flow)に進入してから、その NavGraph 全体がバックスタックからポップされるまで
破棄タイミング フローが完了する、またはユーザーがバックボタン等でフロー開始前の画面に戻り、login_flow がバックスタックから消去されたとき
共有範囲 対象の navigation ブロック(NavGraph)内で定義されたすべての画面

基本的な使い方

各 Destination で navController.getBackStackEntry("login_flow") を使って親グラフの BackStackEntry を取得し、それを viewModel() に渡すことで共有インスタンスを得る。

https://github.com/hir05o1/android-viewmodel-lifecycle/blob/41159f318a94d155d72a406341fad95ec5d15dbb/app/src/main/java/dev/hir05o1/viewmodel_lifecycle/NavGraph.kt#L32-L62

Jetpack Compose における ViewModel のライフサイクル

Composable のライフサイクルと ViewModel は無関係

ViewModel のライフサイクルは Composable と完全に独立している。Composable が再コンポーズされようと、コンポジションから外れようと、ViewModel の生存期間はあくまで ViewModelStoreOwner によって決まる。

ComposableはViewModelStoreOwnerではないため、ViewModel のスコープになることはできない。

Composable ViewModel
生成タイミング コンポジションツリーに追加されたとき ViewModelStoreOwner が生成されたとき
再生成 リコンポーズのたびに関数が再実行される 構成変更では再生成されない
破棄タイミング コンポジションツリーから除去されたとき ViewModelStoreOwner が破棄されたとき

たとえば if (condition) { MyScreen() } という条件付き表示で MyScreen がコンポジションから消えても、その ViewModel は popBackStack() されるまで破棄されない。

viewModel() が参照する ViewModelStoreOwner の決まり方

@Composable
inline fun <reified VM : ViewModel> viewModel(
    viewModelStoreOwner: ViewModelStoreOwner =
        checkNotNull(LocalViewModelStoreOwner.current),
    // ...
): ViewModel

ViewModel.ktより抜粋

LocalViewModelStoreOwner.current が何を指すかは、その Composable がどこに配置されているかによって変わる。

配置場所 LocalViewModelStoreOwner.current
Activity 直下 ComponentActivity
NavHost 内の Destination NavBackStackEntry(各 Destination)
NavGraph スコープ(手動指定) NavBackStackEntry(NavGraph ルート)

Activity 直下での注意点

Navigation を使わない Activity 直下では、LocalViewModelStoreOwner.current は ComponentActivity を指す。これにより、同じ Activity 配下の複数の Composable で viewModel() を呼ぶと、すべてが同一インスタンスを共有してしまう。

setContent {
    Column {
        ScreenA(viewModel = viewModel()) // 同じインスタンス
        ScreenB(viewModel = viewModel()) // 同じインスタンス
    }
}

これを避けるために Compose Navigation を使い、各画面を Destination として定義することが推奨される。

まとめ

ViewModel のライフサイクルは主に以下の3種類の ViewModelStoreOwner と紐づく。

スコープ ViewModelStoreOwner 破棄タイミング
Activity-scoped ComponentActivity Activity が完全に終了したとき
Navigation-scoped NavBackStackEntry(Destination) その画面が BackStack からポップされたとき
NavGraph-scoped NavBackStackEntry(NavGraph ルート) NavGraph 全体が BackStack からポップされたとき

参考

https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja
https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis?hl=ja#compose
https://developer.android.com/guide/components/activities/process-lifecycle?hl=ja
https://github.com/androidx/androidx
https://blog.mindorks.com/android-viewmodels-under-the-hood/

Discussion