Android ViewModel
ViewModel 是 Android 架構中負責管理 UI 資料與處理業務邏輯的核心元件。它的設計初衷是為了讓資料在「組態變更」(如螢幕旋轉)中存續,並作為 UI 與資料層(Repository)之間的橋樑。
為什麼需要 ViewModel?
- 生存於組態變更 (Configuration Changes):Activity 會在旋轉螢幕或切換深色模式時重建,但 ViewModel 會由系統保留,直到 Activity 真正結束。
- 解耦 UI 與資料處理:Activity 應只負責顯示 UI,不應包含獲取資料或計算邏輯。
- 自動清理資源:透過
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)
}
}
最佳實踐與禁忌
- 絕對不可持有 UI 引用:不要在 ViewModel 中儲存
Activity,Fragment或View。這會導致記憶體洩漏 (Memory Leak)。 - 保持 ViewModel 精簡:業務邏輯(如資料過濾、計算)應放在 UseCase 或 Repository 中,ViewModel 僅負責協調。
- 配合 collectAsStateWithLifecycle:在 Compose 中觀察時,請使用
collectAsStateWithLifecycle()以確保 App 切換到背景時停止觀察,節省資源。 - 不可直接暴露可變狀態:永遠不要將
MutableStateFlow直接設為public,必須轉為asStateFlow()暴露給外部。
ViewModel 不是萬能的資料庫。它是過渡性的:用於保留「目前正在查看」的狀態。持久性資料應存入資料庫(Room)或 DataStore 中。