ViewModel 的定位
-
职责 :承载并管理 UI 所需的业务状态与逻辑,让状态跨配置变更(旋转、深浅色、语言切换)不丢失。
-
不该做:不持有 View/Fragment/Activity 引用,不做长时前后台任务(那是 WorkManager 的活)。
-
与生命周期关系:跟随 ViewModelStoreOwner(如 Activity、Fragment、NavBackStackEntry)存活;被移出 Store 时调用 onCleared()。
组成与运行机制(底层视角)
-
ViewModelStore / Owner:FragmentActivity、Fragment、NavBackStackEntry 都实现了 ViewModelStoreOwner,内部有个 ViewModelStore(本质是个以 key 为索引的 Map),存放实例。
-
ViewModelProvider:按 key + Factory 创建或复用实例;默认 key 形式:DefaultKey:<完整类名>。
-
Factory 与 CreationExtras(2.5+):
- Factory.create(modelClass, extras) 可拿到 SavedStateRegistryOwner、ViewModelStoreOwner、Application 等上下文。
- AndroidViewModel 需要 Application;SavedStateHandle 需要 SavedStateRegistryOwner。
-
onCleared() :当 Owner 销毁(或从导航栈移除)时触发;要在这里释放资源、取消协程。
与 SavedState:进程死亡后的"可恢复"
- SavedStateHandle :面向 ViewModel 的"可保存 Bundle"。配置变更无感;进程被系统杀死后重建时,能把指定键的值恢复。
- 可保存的类型:基本类型、String、Bundle、Parcelable、Serializable、对应数组/ArrayList。自定义类型请实现 Parcelable 或做序列化。
- 常见用法
kotlin
@HiltViewModel
class DetailVM @Inject constructor(
private val repo: Repo,
private val saved: SavedStateHandle
) : ViewModel() {
// 可观察 + 可恢复(进程重启后还能拿到)
val query = saved.getStateFlow("query", "")
fun setQuery(newQ: String) {
saved["query"] = newQ
}
}
协程与数据流:viewModelScope/MVI/单次事件
- viewModelScope :默认 SupervisorJob + Dispatchers.Main.immediate,子协程失败不会取消兄弟任务;在 onCleared() 自动取消。
- 推荐状态模型(MVI 思路)
kotlin
data class UiState(
val loading: Boolean = false,
val items: List<Item> = emptyList(),
val error: String? = null
)
@HiltViewModel
class ListVM @Inject constructor(private val repo: Repo) : ViewModel() {
private val _ui = MutableStateFlow(UiState())
val ui: StateFlow<UiState> = _ui
init { refresh() }
fun refresh() = viewModelScope.launch {
_ui.update { it.copy(loading = true, error = null) }
runCatching { repo.load() }
.onSuccess { list -> _ui.update { it.copy(loading = false, items = list) } }
.onFailure { e -> _ui.update { it.copy(loading = false, error = e.message) } }
}
}
-
一次性事件(Toast/导航等) :避免 SingleLiveEvent。推荐:
- Channel(capacity = Channel.BUFFERED) + receiveAsFlow();
- 或 MutableSharedFlow(extraBufferCapacity = 1, replay = 0)。
kotlin
private val _effect = MutableSharedFlow<Effect>(extraBufferCapacity = 1)
val effect = _effect.asSharedFlow()
fun onItemClick(id: Long) {
_effect.tryEmit(Effect.NavigateToDetail(id))
}
在不同宿主中的使用法
Fragment/Activity(View 系统)
- 单 Fragment 内:
csharp
private val vm: ListVM by viewModels()
- 多个 Fragment 共享同一个 VM(同 Activity 范围) :
csharp
private val vm: SharedVM by activityViewModels()
- 按导航图范围共享(Fragment + Navigation) :
kotlin
private val vm: SharedVM by navGraphViewModels(R.id.home_graph)
Compose
- 最常见:
kotlin
@Composable
fun ListScreen(
vm: ListVM = hiltViewModel() // 或 viewModel()
) {
val ui by vm.ui.collectAsStateWithLifecycle()
// 渲染 ui
}
- 在 Navigation-Compose 中按父图共享:
ini
val navController = rememberNavController()
NavHost(navController, startDestination = "child") {
navigation(startDestination = "child", route = "parent") {
composable("child") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("parent")
}
val vm = hiltViewModel<SharedVM>(parentEntry)
// 使用 vm(在整个 parent 图范围共享)
}
}
}
依赖注入与自定义构造
- Hilt(推荐) :@HiltViewModel + 构造函数注入,想要 SavedStateHandle 直接写在参数里即可。
- 没有 Hilt 时:自定义 ViewModelProvider.Factory 或使用 AbstractSavedStateViewModelFactory 以便传参与拿到 SavedStateHandle。
kotlin
class MyFactory(
owner: SavedStateRegistryOwner,
private val repo: Repo
) : AbstractSavedStateViewModelFactory(owner, null) {
override fun <T : ViewModel> create(
key: String, modelClass: Class<T>, state: SavedStateHandle
): T = DetailVM(repo, state) as T
}
与生命周期的正确配合(收集/取消)
- Fragment(View 系统) :用 viewLifecycleOwner 而非 this。
scss
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.ui.collect { render(it) }
vm.effect.collect { handle(it) }
}
}
- Compose:collectAsStateWithLifecycle() + LaunchedEffect 处理一次性事件。
scss
val lifecycle = LocalLifecycleOwner.current.lifecycle
LaunchedEffect(Unit) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.effect.collect { handle(it) }
}
}
常见坑与最佳实践
-
不要持有 Context/Fragment/Activity。若需全局上下文,用 Application(AndroidViewModel 或 @ApplicationContext 注入)。
-
大对象/Bitmap/Adapter 放 VM? 不推荐。VM 保持"轻 业务、重状态";大资源放仓库/缓存层,或按需获取。
-
LiveData 还是 Flow? 新项目首选 Flow + collectAsStateWithLifecycle;老项目可桥接 asLiveData() / asFlow()。
-
一次性事件别用 LiveData(会粘性/丢失/旋转触发),用 SharedFlow/Channel。
-
长时任务 (>10 分钟、前后台、重试策略)用 WorkManager。
-
onCleared 不一定总被调用(比如进程被直接杀死);重要状态要写入持久层或 SavedStateHandle。
-
Nav 图范围共享优先于 activityViewModels():粒度更小、生命周期更贴合业务边界。
-
测试:
-
协程:kotlinx-coroutines-test 的 runTest。
-
Flow:app.cash.turbine 验证发射顺序与内容。
-
LiveData:InstantTaskExecutorRule。
-
通过构造注入替换仓库为 fake/mock,让 VM 可单测。
-
典型"模板"
kotlin
// 1) 状态 + 侧效应
data class UiState(val loading: Boolean = false, val data: List<Item> = emptyList(), val err: String? = null)
sealed interface Effect { data class NavToDetail(val id: Long) : Effect }
// 2) ViewModel
@HiltViewModel
class SampleVM @Inject constructor(
private val repo: Repo,
private val saved: SavedStateHandle
) : ViewModel() {
private val _ui = MutableStateFlow(UiState())
val ui: StateFlow<UiState> = _ui
private val _effect = MutableSharedFlow<Effect>(extraBufferCapacity = 1)
val effect = _effect.asSharedFlow()
init { load() }
fun load() = viewModelScope.launch {
_ui.update { it.copy(loading = true, err = null) }
runCatching { repo.fetch() }
.onSuccess { list ->
_ui.update { it.copy(loading = false, data = list) }
}
.onFailure { e ->
_ui.update { it.copy(loading = false, err = e.message) }
}
}
fun onClick(id: Long) {
_effect.tryEmit(Effect.NavToDetail(id))
saved["last_click_id"] = id // 进程重启后可恢复
}
}
// 3) Compose 层
@Composable
fun SampleScreen(vm: SampleVM = hiltViewModel()) {
val ui by vm.ui.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
vm.effect.collect { eff ->
when (eff) {
is Effect.NavToDetail -> {/* navController.navigate(...) */}
}
}
}
/* 渲染 ui */
}
何时选择哪种作用域
- 只被单个 Fragment 使用:by viewModels()
- 多个 Fragment 同 Activity 共享:by activityViewModels()
- 多个目的地共享(Navigation) :by navGraphViewModels(graphId) / 在 Compose 用 parentEntry + hiltViewModel(parentEntry)
- 全局级别:尽量避免"全局 VM",改为仓库 + 单例缓存 + DataStore/Room。