jetpack ViewModel

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) }
    }
}

常见坑与最佳实践

  1. 不要持有 Context/Fragment/Activity。若需全局上下文,用 Application(AndroidViewModel 或 @ApplicationContext 注入)。

  2. 大对象/Bitmap/Adapter 放 VM? 不推荐。VM 保持"轻 业务、重状态";大资源放仓库/缓存层,或按需获取。

  3. LiveData 还是 Flow? 新项目首选 Flow + collectAsStateWithLifecycle;老项目可桥接 asLiveData() / asFlow()。

  4. 一次性事件别用 LiveData(会粘性/丢失/旋转触发),用 SharedFlow/Channel。

  5. 长时任务 (>10 分钟、前后台、重试策略)用 WorkManager。

  6. onCleared 不一定总被调用(比如进程被直接杀死);重要状态要写入持久层或 SavedStateHandle。

  7. Nav 图范围共享优先于 activityViewModels():粒度更小、生命周期更贴合业务边界。

  8. 测试:

    • 协程: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。
相关推荐
夏幻灵6 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
时间的拾荒人11 小时前
Qt 界面优化:绘图详解 —— 从基础图形到图片处理
开发语言·qt·面试
沐言人生13 小时前
又一个神级考公脑库-22,375篇真题终于按考点整理了
后端·面试
for_ever_love__13 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结
救救孩子把14 小时前
Agent面试题-记忆治理-上下文-多Agent协作与工程落地
面试·agent·多agent·agent记忆·agent面试题·上下文治理
泡干脆面就番茄14 小时前
HDFS读写流程与MapReduce原理深度解析(面试高频考点)
hdfs·面试·mapreduce
bksczm14 小时前
0基础面试3
面试·职场和发展
老王爱玩车17 小时前
动态内存管理
c语言·开发语言·学习·面试
海绵宝宝转agent17 小时前
2026-10-9 leetcode100刷题+面经整理
笔记·算法·面试
JAVA面经实录91718 小时前
Java高级后端 · 全套面试通关手册(线上故障排查)
java·jvm·面试