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。
相关推荐
渣哥4 小时前
Lazy能否有效解决循环依赖?答案比你想的复杂
javascript·后端·面试
前端架构师-老李5 小时前
面试问题—你接受加班吗?
面试·职场和发展
ANYOLY5 小时前
多线程&并发篇面试题
java·面试
南北是北北5 小时前
RecyclerView 的数据驱动更新
面试
uhakadotcom5 小时前
coze的AsyncTokenAuth和coze的TokenAuth有哪些使用的差异?
后端·面试·github
Chejdj5 小时前
StateFlow、SharedFlow 和LiveData区别
android·面试
道可到6 小时前
直接可以拿来的面经 | 从JDK 8到JDK 21:一次团队升级的实战经验与价值复盘
java·面试·架构
南北是北北6 小时前
RecyclerView 进阶绑定:多类型 / 局部刷新(payload)/ 稳定 ID
面试
Hilaku6 小时前
为什么我开始减少逛技术社区,而是去读非技术的书?
前端·javascript·面试