直接 new GameViewModel()
Kotlin
class GameViewModel : ViewModel() {
var score by mutableStateOf(0)
init { println("ViewModel init!") } // 每次重組都會執行
override fun onCleared() { println("ViewModel cleared!") } // 永遠不會被系統呼叫
}
@Composable
fun GameScreenWrong(gameViewModel: GameViewModel = GameViewModel()) {
// ... UI 顯示 gameViewModel.score ...
Button(onClick = { gameViewModel.incrementScore() }) { /* ... */ }
}
使用 viewModel() 函數
Kotlin
@Composable
fun GameScreenCorrect(
gameViewModel: GameViewModel = viewModel() // <-- 正確方式!
) {
// ... UI 顯示 gameViewModel.score ...
Button(onClick = { gameViewModel.incrementScore() }) { /* ... */ }
}
viewModel() 的運作機制:
viewModel() 是一個 Composable 函數,它會:
- 尋找 ViewModelStoreOwner: 向上查找最近的 Activity、Fragment 或 NavBackStackEntry。
- 利用 ViewModelProvider: 透過 ViewModelProvider 取得或創建 ViewModel 實例。如果已存在,則返回現有實例;如果沒有,則創建並存儲,確保與其 ViewModelStoreOwner 生命週期綁定。
- 綁定生命週期: 這樣獲取的 ViewModel 會在 ViewModelStoreOwner 銷毀時由框架自動清理,並呼叫 onCleared()。
你可能會問 GameViewModel 明明就是繼承 ViewModel
當你讓 GameViewModel 繼承 ViewModel 時,你是在向 Android 框架宣告:「我這個 GameViewModel 是一個特殊的類別,它需要被框架的生命週期機制所管理。」
這個繼承關係本身並沒有讓你的 GameViewModel 獲得魔法,而是告訴框架:「我是一個 ViewModel,請你用處理 ViewModel 的方式來處理我。」
想像一下:
- ViewModel 是一個介面或抽象類別,定義了「如何被框架管理」的合約。 它包含像 onCleared() 這樣的方法,這些方法只有在框架管理它時才會有意義。
- 你繼承 ViewModel,就像是你的 GameViewModel 穿上了寫著「我是生命週期管理物件」的制服。
然而,單純穿上制服並不代表你就自動被管理了。你還需要被「註冊」到管理系統中。
問題的根源:沒有被「註冊」到 ViewModelStore
GameViewModel 繼承 ViewModel 固然是第一步,但光是繼承,你的 GameViewModel 實例並不會自動被 Android 框架的 ViewModelStore 所持有和管理。
ViewModelStore 是 Android 框架中一個非常重要的組件,它負責:
- 儲存 ViewModel 實例: 在配置變更(例如螢幕旋轉)時,保留 ViewModel 實例,使其不被銷毀。
- 綁定生命週期: 監聽其宿主(如 Activity 或 Fragment)的生命週期,當宿主永久銷毀時,會呼叫其中所有 ViewModel 的 onCleared() 方法來釋放資源。
1. 你手動 new GameViewModel():
Kotlin
class GameViewModel : ViewModel() { /* ... */ }
@Composable
fun GameScreenWrong() {
val gameViewModel = GameViewModel() // <-- 直接 new
// ...
}
- 繼承了 ViewModel: GameViewModel 確實繼承了 ViewModel,它有 onCleared() 方法,也能使用 viewModelScope。
- 但沒有被「註冊」: 你直接 new 了一個 GameViewModel 物件。這個物件是獨立存在的,它並沒有被任何 ViewModelStore 所知道或持有。它就像你私人創建的一個「穿著制服的物件」,但並沒有進入那個「需要被管理的隊伍」中。
- 結果:
- 當 GameScreenWrong 重組時,它會再次 new 一個新的 GameViewModel,因為舊的實例沒有被 ViewModelStore 持有和返回,所以狀態會遺失。
- 當 GameScreenWrong 所在的 Activity 或 Fragment 銷毀時,ViewModelStore 不知道你這個手動 new 的 GameViewModel 存在,所以永遠不會呼叫它的 onCleared() 方法,導致記憶體洩漏。
2. 使用 viewModel() 函數:
Kotlin
@Composable
fun GameScreenCorrect(
gameViewModel: GameViewModel = viewModel() // <-- 透過 viewModel()
) {
// ...
}
- 繼承了 ViewModel: GameViewModel 依然繼承 ViewModel。
- 被 viewModel() 「註冊」了:viewModel() 這個 Compose 函數的內部會:
- 找到最近的 ViewModelStoreOwner (通常是 Activity 或 Fragment)。
- 透過 ViewModelProvider (它會訪問 ViewModelStoreOwner 內部的 ViewModelStore),去獲取或創建 GameViewModel 實例。
- 如果創建了新的實例,它會將這個實例「註冊」到對應的 ViewModelStore 中。
- 結果:
- 因為被 ViewModelStore 持有,所以在重組或配置更改時,viewModel() 會從 ViewModelStore 中返回同一個 GameViewModel 實例,狀態得以保留。
- 當 Activity 或 Fragment 永久銷毀時,ViewModelStore 會自動呼叫其內部所有 ViewModel 的 onCleared() 方法,確保資源被正確釋放。
val gameViewModel = viewModel()
val gameViewModel = ViewModelProvider(viewModelStoreOwner, factory).get(GameViewModel::class.java)
其實就是這樣啦上面兩行是等價的
—