Android ViewModel

ViewModelAndroid 架構中負責管理 UI 資料與處理業務邏輯的核心元件。它的設計初衷是為了讓資料在「組態變更」(如螢幕旋轉)中存續,並作為 UI 與資料層(Repository)之間的橋樑。

為什麼需要 ViewModel?

  1. 生存於組態變更 (Configuration Changes):Activity 會在旋轉螢幕或切換深色模式時重建,但 ViewModel 會由系統保留,直到 Activity 真正結束。
  2. 解耦 UI 與資料處理:Activity 應只負責顯示 UI,不應包含獲取資料或計算邏輯。
  3. 自動清理資源:透過 viewModelScope 啟動的協程會在 ViewModel 銷毀時自動取消,避免記憶體洩漏。

基本實作與 UDF 架構

現代 Android 開發建議採用 單向資料流 (Unidirectional Data Flow, UDF)

  • State 出 (State Out):ViewModel 向外暴露一個 StateFlow
  • Event 入 (Events In):UI 透過呼叫 ViewModel 的函式來觸發邏輯。
class UserViewModel(private val repository: UserRepository) : ViewModel() {

    // 內部使用 MutableStateFlow,確保只有內部能修改
    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    
    // 外部僅能觀察唯讀的 StateFlow
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UserUiState.Loading
            try {
                val users = repository.getUsers()
                _uiState.value = UserUiState.Success(users)
            } catch (e: Exception) {
                _uiState.value = UserUiState.Error(e.message)
            }
        }
    }
}

進階主題:處理更複雜的場景

SavedStateHandle (對抗處理程序終止)

除了螢幕旋轉,系統可能會因為記憶體不足而終止你的 App 進程。SavedStateHandle 能在進程重啟後恢復關鍵資料(如搜尋關鍵字或選中的 ID)。

class SearchViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    // 從 SavedStateHandle 中讀取,並能自動存回
    val searchQuery = savedStateHandle.getStateFlow("query", "")

    fun updateQuery(newQuery: String) {
        savedStateHandle["query"] = newQuery
    }
}

Hilt 依賴注入

在實務專案中,ViewModel 通常由 Hilt 管理。

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: MyRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() { ... }

// 在 Compose 中呼叫
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) { ... }

ViewModel 的作用域 (Scoping)

並非所有 ViewModel 都要跟著 Activity:

  • Activity Scoped: 不同的 Fragment 共用同一個 ViewModel(用於跨頁面傳輸資料)。
  • NavGraph Scoped: ViewModel 的生命週期與特定的導航分圖(Navigation Graph)綁定。

ViewModel 的單元測試 (Testing)

測試 ViewModel 非常簡單,因為它不依賴 Android Framework(如果你沒有傳入 Context)。

class UserViewModelTest {
    // 必須使用 MainDispatcherRule 來替換主線程調度器
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun loadUsers_success_updatesUiState() = runTest {
        val repository = FakeUserRepository()
        val viewModel = UserViewModel(repository)
        
        viewModel.loadUsers()
        
        // 驗證狀態是否正確變化
        assertEquals(UserUiState.Success(fakeUsers), viewModel.uiState.value)
    }
}

最佳實踐與禁忌

  1. 絕對不可持有 UI 引用:不要在 ViewModel 中儲存 Activity, FragmentView。這會導致記憶體洩漏 (Memory Leak)。
  2. 保持 ViewModel 精簡:業務邏輯(如資料過濾、計算)應放在 UseCase 或 Repository 中,ViewModel 僅負責協調。
  3. 配合 collectAsStateWithLifecycle:在 Compose 中觀察時,請使用 collectAsStateWithLifecycle() 以確保 App 切換到背景時停止觀察,節省資源。
  4. 不可直接暴露可變狀態:永遠不要將 MutableStateFlow 直接設為 public,必須轉為 asStateFlow() 暴露給外部。
ViewModel 不是萬能的資料庫。它是過渡性的:用於保留「目前正在查看」的狀態。持久性資料應存入資料庫(Room)或 DataStore 中。