Skip to main content

fun GameScreen(gameViewModel: GameViewModel = GameViewModel()) 這樣寫有啥問題?

直接 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 函數,它會:

  1. 尋找 ViewModelStoreOwner: 向上查找最近的 Activity、Fragment 或 NavBackStackEntry。
  2. 利用 ViewModelProvider: 透過 ViewModelProvider 取得或創建 ViewModel 實例。如果已存在,則返回現有實例;如果沒有,則創建並存儲,確保與其 ViewModelStoreOwner 生命週期綁定。
  3. 綁定生命週期: 這樣獲取的 ViewModel 會在 ViewModelStoreOwner 銷毀時由框架自動清理,並呼叫 onCleared()。

你可能會問 GameViewModel 明明就是繼承 ViewModel

當你讓 GameViewModel 繼承 ViewModel 時,你是在向 Android 框架宣告:「我這個 GameViewModel 是一個特殊的類別,它需要被框架的生命週期機制所管理。」

這個繼承關係本身並沒有讓你的 GameViewModel 獲得魔法,而是告訴框架:「我是一個 ViewModel,請你用處理 ViewModel 的方式來處理我。」

想像一下:

  • ViewModel 是一個介面或抽象類別,定義了「如何被框架管理」的合約。 它包含像 onCleared() 這樣的方法,這些方法只有在框架管理它時才會有意義。
  • 你繼承 ViewModel,就像是你的 GameViewModel 穿上了寫著「我是生命週期管理物件」的制服。

然而,單純穿上制服並不代表你就自動被管理了。你還需要被「註冊」到管理系統中


問題的根源:沒有被「註冊」到 ViewModelStore

GameViewModel 繼承 ViewModel 固然是第一步,但光是繼承,你的 GameViewModel 實例並不會自動被 Android 框架的 ViewModelStore 所持有和管理。

ViewModelStore 是 Android 框架中一個非常重要的組件,它負責:

  1. 儲存 ViewModel 實例: 在配置變更(例如螢幕旋轉)時,保留 ViewModel 實例,使其不被銷毀。
  2. 綁定生命週期: 監聽其宿主(如 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 函數的內部會:
    1. 找到最近的 ViewModelStoreOwner (通常是 Activity 或 Fragment)。
    2. 透過 ViewModelProvider (它會訪問 ViewModelStoreOwner 內部的 ViewModelStore),去獲取或創建 GameViewModel 實例。
    3. 如果創建了新的實例,它會將這個實例「註冊」到對應的 ViewModelStore 中
  • 結果:
    • 因為被 ViewModelStore 持有,所以在重組或配置更改時,viewModel() 會從 ViewModelStore 中返回同一個 GameViewModel 實例,狀態得以保留。
    • 當 Activity 或 Fragment 永久銷毀時,ViewModelStore 會自動呼叫其內部所有 ViewModel 的 onCleared() 方法,確保資源被正確釋放。
val gameViewModel = viewModel()
val gameViewModel = ViewModelProvider(viewModelStoreOwner, factory).get(GameViewModel::class.java)

其實就是這樣啦上面兩行是等價的