Android Jetpack 系列(八)ViewModel 视图模型
1. 背景在应用开发中随着业务逻辑的复杂化我们往往需要管理越来越多的数据状态例如网络请求结果、界面输入内容、滚动位置、用户交互状态等。这些数据通常会随着 Activity 或 Fragment 的生命周期而变化如果处理不当就容易出现数据丢失、界面闪烁、甚至内存泄漏等问题。为了解决这些痛点我们先简单回顾一下常见的应用架构演进过程。MVCModel-View-ControllerMVC 是最早期的一种架构设计View 负责界面展示Model 负责数据处理Controller 负责协调二者。但在 Android 中Activity/Fragment 通常既充当 View又承担 Controller 的部分逻辑结果导致代码臃肿UI 与业务逻辑高度耦合。MVPModel-View-PresenterMVP 在 Android 中得到了广泛应用Presenter 负责业务逻辑View 只负责渲染界面。虽然一定程度上解决了 Activity 的臃肿问题但 Presenter 与 View 仍存在强引用关系生命周期绑定困难。例如当 Activity 重建屏幕旋转时Presenter 需要重新创建、重新绑定状态恢复变得繁琐。MVVMModel-View-ViewModelMVVM 架构中引入了 ViewModel它从视图层中抽离出“可感知生命周期的状态持有者”通过数据驱动通常搭配 LiveData来自动更新 UI。这让 UI 逻辑更轻数据与生命周期解耦成为现代 Android 推荐的架构模式之一。2. 了解 ViewModel2.1 ViewModel 是什么ViewModel 是 Android Jetpack 架构组件中的核心之一它是一个专为保存和管理界面状态UI State 而设计的类。它与 Activity 或 Fragment 的生命周期绑定但可以在配置变化如屏幕旋转时保持数据不丢失。简单来说ViewModel UI 状态缓存器 业务逻辑中枢。2.2 ViewModel 的作用与优势1.状态持久性保存界面相关数据避免因配置变化导致丢失。2.业务逻辑集中将业务逻辑与界面解耦Activity 仅负责展示提升架构清晰度与可测试性。3.生命周期感知不用持有 View 引用可与 LiveData 搭配实现数据驱动 UI3. 实现 ViewModel下面通过一个简单的示例来演示如何在项目中使用 ViewModel。3.1. 定义 ViewModel 类class CounterViewModel : ViewModel() { private var _count MutableLiveData(0) val count: LiveDataInt get() _count fun increase() { _count.value (_count.value ?: 0) 1 } }这里我们定义了一个 CounterViewModel用于保存计数数据。它内部持有一个MutableLiveData对外只暴露不可变的LiveData体现“数据内部可改、外部只读”的设计原则。3.2. 在 Activity 中使用3.2.1. 观察数据变化class MainActivity : AppCompatActivity() { private lateinit var viewModel: CounterViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val textCount findViewByIdTextView(R.id.text_count) val buttonAdd findViewByIdButton(R.id.button_add) viewModel ViewModelProvider(this)[CounterViewModel::class.java] // 观察 ViewModel 中的 count 变化 viewModel.count.observe(this) { value - textCount.text Count: $value } buttonAdd.setOnClickListener { viewModel.increase() } } }ViewModel 本身只负责数据管理不直接更新界面。通过与 LiveData 搭配UI 层可以观察数据变化实现自动刷新。3.2.3. 结合 DataBinding 使用 ViewModel如果你在项目中启用了 DataBinding可以直接在 XML 中绑定 ViewModel?xml version1.0 encodingutf-8? layout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:toolshttp://schemas.android.com/tools data variable nameviewModel typecom.zyx.viewmodel.demo.CounterViewModel / /data LinearLayout android:idid/main android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:paddingTop120dp tools:context.MainActivity TextView android:idid/text_count android:layout_widthwrap_content android:layout_heightwrap_content android:text{Count: String.valueOf(viewModel.count)} android:textSize20sp / Button android:idid/button_add android:layout_widthwrap_content android:layout_heightwrap_content android:onClick{() - viewModel.increase()} android:text增加计数 / /LinearLayout /layoutActivity 绑定class MainActivity : AppCompatActivity() { private lateinit var viewModel: CounterViewModel private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel ViewModelProvider(this)[CounterViewModel::class.java] binding DataBindingUtil.setContentView(this, R.layout.activity_main) binding.lifecycleOwner this binding.viewModel viewModel } }说明binding.lifecycleOwner this让绑定自动响应 LiveData 更新。结合 ViewModel LiveData DataBinding可以实现响应式 UI。4. ViewModel 的作用域4.1. 如何创建 ViewModel 实例上面示例中 Activity 中通过 ViewModelProvider(this)[xxx::class.java] 便创建并获取 ViewModel 实例。这里的 this 是 Activity 本身它是一个ViewModelStoreOwner对象接着中括号[ ] 对应其 get 方法。但在 Kotlin 中Jetpack 已经为常见场景如 Activity、Fragment、Compose提供了更简洁的扩展函数by viewModels() 。 系统会自动根据当前的 ViewModelStore 创建并管理 ViewModel 实例。class MainActivity : AppCompatActivity() { // private lateinit var viewModel: CounterViewModel private val viewModel: CounterViewModel by viewModels () override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel ViewModelProvider(this)[CounterViewModel::class.java] } }这些扩展函数底层仍是通过 ViewModelProvider 实现的只是使用了 Kotlin 的委托语法进行封装。by viewModels() 扩展函数分别定义在 androidx.activity.ComponentActivityActivity 场景与 androidx.fragment.app.FragmentFragment 场景中若找不到类可手动添加依赖dependencies { implementation androidx.activity:activity-ktx:1.9.2 implementation androidx.fragment:fragment-ktx:1.8.2 }4.2. 理解作用域ViewModel 的生命周期依附于实现了 ViewModelStoreOwner 的组件如 Activity 或 Fragment而非其视图。作用域决定了数据能保存多久是否能在多个界面共享 ViewModel 实例。4.3. ViewModelStoreOwnerViewModelStoreOwner 是一个接口定义了一个组件是否能“拥有” ViewModel。Activity 和 Fragment 都实现了它因此都可以作为 ViewModel 的持有者。通常情况下我们获取的 ViewModel 都限定在最近的 ViewModelStoreOwner 上。例如在 Activity 中使用 by viewModels()则作用域限定为该 Activity在 Fragment 中使用 by viewModels()则作用域限定为该 Fragment。有时我们希望手动指定作用域比如让子 Fragment 共享父 Fragment 的 ViewModel。这时可以使用 ownerProducer 参数来决定作用域的归属。class MyFragment : Fragment() { // ViewModel 作用域限定为父 Fragment val viewModel: MyViewModel by viewModels( ownerProducer { requireParentFragment() } ) }同理如果要从 Fragment 获取作用域限定为 Activity 的 ViewModel可以使用 by activityViewModels()class MyFragment : Fragment() { // ViewModel 作用域限定为宿主 Activity val viewModel: MyViewModel by activityViewModels() }这是一种常见的跨 Fragment 共享 ViewMode 的方式例如在多个页面之间共享用户选择或搜索条件。5. ViewModel 的生命周期5.1. 生命周期关系ViewModel 的生命周期与其宿主即 ViewModelStoreOwner绑定。对 Activity 而言当 Activity 销毁时ViewModel 才会被清除对 Fragment 而言当 Fragment 从 FragmentManager 分离且不再返回时ViewModel 才会被清除。下图说明了activity 经历屏幕旋转而后结束时所处的各种生命周期状态。该图还在关联的 activity 生命周期的旁边显示了 ViewModel 的生命周期。此图表说明了 activity 的各种状态。这些基本状态同样适用于 fragment 的生命周期。通常在系统首次调用 activity 对象的 onCreate() 方法时请求 ViewModel。系统可能会在 activity 的整个生命周期内多次调用 onCreate()如在旋转设备屏幕时。ViewModel 存在的时间范围是从你首次请求 ViewModel 直到 activity 完成并销毁。当 Activity 被真正销毁例如用户退出页面、进程被杀时系统才会调用 ViewModel 的 onCleared() 方法override fun onCleared() { super.onCleared() Log.d(CounterViewModel, onCleared called) }可以在这里释放资源、取消协程或关闭流。5.2. 协程作用域 viewModelScopeViewModel 自带一个 viewModelScope 协程作用域完整路径是androidx.lifecycle.viewModelScope它的生命周期与 ViewModel 的生命周期同步会在 ViewModel 被销毁时也自动取消从而避免内存泄漏情况class CounterViewModel : ViewModel() { …… fun loadData() { viewModelScope.launch { val result repository.getData() _count.value result.size } } }这让 ViewModel 天然适合与 Kotlin 协程结合使用异步逻辑更加安全简洁。如果你想使用自定义作用域而不是 androidx.lifecycle.viewModelScope使测试更简单ViewModel 可以在其构造函数中接收 CoroutineScope 作为依赖项。如果 ViewModelStoreOwner 在 ViewModel 的生命周期结束时清除 ViewModelViewModel 也会取消 CoroutineScope。class CounterViewModel(private val viewModelScope: CoroutineScope CoroutineScope(SupervisorJob() Dispatchers.Main.immediate)) : ViewModel() { private var _count MutableLiveData(0) val count: LiveDataInt get() _count fun increase() { _count.value (_count.value ?: 0) 1 } fun loadData() { viewModelScope.launch { val result repository.getData() _count.value result.size } } override fun onCleared() { viewModelScope.cancel() } }从 Lifecycle 版本 2.5 及更高版本开始你可以将一个或多个 Closeable 对象传递给在清除 ViewModel 实例时自动关闭的 ViewModel 构造函数。class CloseableCoroutineScope(context: CoroutineContext SupervisorJob() Dispatchers.Main.immediate) : Closeable, CoroutineScope { override val coroutineContext: CoroutineContext context override fun close() { coroutineContext.cancel() } } open class BaseViewModel(protected val viewModelScope: CoroutineScope CloseableCoroutineScope()) : ViewModel(viewModelScope as Closeable?) { init { if (viewModelScope is Closeable) { // 可关闭 ViewModelScope在 CloseableCoroutineScope 内的 close 方法内已执行了 coroutineContext.cancel()所以不再需要在 onCleared 生命周期去 cancel super.addCloseable(viewModelScope) } } // /** // * ViewModel 销毁如果用 addCloseable 就不需要了 viewModelScope.cancel() // */ // override fun onCleared() { // super.onCleared() // viewModelScope.cancel() // } } class CounterViewModel() : BaseViewModel() { private var _count MutableLiveData(0) val count: LiveDataInt get() _count fun increase() { _count.value (_count.value ?: 0) 1 } fun loadData() { viewModelScope.launch { val result repository.getData() _count.value result.size } } }6. 创建具有参数的 ViewModel在实际项目中很多 ViewModel 不只是简单地保存界面状态还可能需要依赖仓库Repository或者其他外部参数这些依赖往往涉及数据访问层、网络层或系统服务。此时就需要通过自定义 ViewModelProvider.Factory 接口实现替换 create(ClassT, CreationExtras) 方法来创建带参数的 ViewModel 实例。6.1. 传入 Application 参数以下示例展示了如何创建 ViewModel 实例时传入 Application 对象作为参数class CounterViewModel(val application: Application) : ViewModel() { companion object { val Factory: ViewModelProvider.Factory object : ViewModelProvider.Factory { Suppress(UNCHECKED_CAST) override fun T : ViewModel create(modelClass: ClassT, extras: CreationExtras): T { val application checkNotNull(extras[APPLICATION_KEY]) return CounterViewModel(application) as T } } } …… }或者使用 ViewModel 工厂 DSL 借助更惯用的 Kotlin API 创建工厂class CounterViewModel(val application: Application) : ViewModel() { companion object { val Factory: ViewModelProvider.Factory viewModelFactory { initializer { val application checkNotNull(this[APPLICATION_KEY]) CounterViewModel(application) } } } …… }Activity中创建 ViewModel 对象private val viewModel: CounterViewModel by viewModels { CounterViewModel.Factory }或者使用传统的创建 ViewModelProvider 对象时传入 ViewModelProvider.Factory 对象作为参数private val viewModel ViewModelProvider(this, CounterViewModel.Factory)[CounterViewModel::class.java]6.2. 传入自定义参数你可以通过创建自定义键通过 CreationExtras 将参数传递给 ViewModel。在此示例中ViewModel 定义了一个自定义键并在 ViewModelProvider.Factory 中使用该键。class CounterViewModel(val application: Application, val repository: CounterRepository) : ViewModel() { companion object { val MY_REPOSITORY_KEY object : CreationExtras.KeyCounterRepository {} val Factory: ViewModelProvider.Factory viewModelFactory { initializer { val application checkNotNull(this[APPLICATION_KEY]) val repository this[MY_REPOSITORY_KEY] as CounterRepository CounterViewModel(application, repository) } } } …… }Activity中创建 ViewModel 实例并传入参数private val repository CounterRepository() val creationExtras MutableCreationExtras().apply { set(CounterViewModel.MY_REPOSITORY_KEY, repository) } private val viewModel ViewModelProvider(this.viewModelStore, CounterViewModel.Factory, creationExtras)[CounterViewModel::class.java]7. ViewModel 的已保存状态模块ViewModel 能自动在配置更改例如横竖屏切换时保留数据。但是如果系统因为资源不足而杀掉应用的进程ViewModel 里的数据就会被清空。7.1. SavedStateHandle 是什么SavedStateHandle 是一个自动保存的键值映射表可在进程被系统终止后恢复上次状态。savedStateHandle[query] apple val lastQuery savedStateHandle[query] ?: val lastQuery2 savedStateHandle.getString(query)这些数据会在系统终止进程后重新创建 ViewModel 时 自动恢复回来。注意它保存的数据是「任务堆栈task stack」级别的。如果你强制停止应用、从「最近任务」移除应用或重启设备保存的状态都会丢失。7.1.1. 常用操作SavedStateHandle 的常用操作有方法作用set(key, value)保存值get(key)读取值contains(key)检查是否存在某键remove(key)删除指定键的值keys()返回所有键名7.1.2. 支持的数据类型SavedStateHandle 内部是基于 Bundle 实现的因此支持所有 Bundle 可序列化的类型。支持类型数组支持Double / Int / Long / String / Float / Short / Byte / Char / CharSequence是Parcelable / Serializable是SparseArray / Binder / Bundle / ArrayListSize / SizeFAPI 21对于自定义类建议使用 Parcelize 注解实现 Parcelable。如果对象既不是 Parcelable 也不是 Serializable可以自己注册保存逻辑savedStateHandle.setSavedStateProvider(key) { Bundle().apply { putString(path, file.absolutePath) } }系统保存状态时会自动调用这个 provider 的 saveState() 方法。重新创建后你可以通过 savedStateHandle.getBundle(key) 恢复。完整示例保存临时文件路径private fun File.saveTempFile() bundleOf(path, absolutePath) private fun Bundle.restoreTempFile() getString(path)?.let { File(it) } class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? savedStateHandle.getBundle(temp_file)?.restoreTempFile() init { savedStateHandle.setSavedStateProvider(temp_file) { tempFile?.saveTempFile() ?: Bundle() } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile(temp, null).also { tempFile it } } }7.2.在 ViewModel 中使用从 Fragment 1.2.0 / Activity 1.1.0 起只要你声明了这样的构造函数class CounterViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { ... }系统就会自动帮你注入一个 SavedStateHandle 对象。然后照常一样创建 ViewModelclass MainActivity : AppCompatActivity() { private val viewModel: CounterViewModel by viewModels () }默认的 ViewModelProvider.Factory 能自动为具有 SavedStateHandle 构造函数的 ViewModel 注入实例。如果你需要创建具有参数的 ViewModel要自定义 ViewModelProvider.Factory那么可以用CreationExtras.createSavedStateHandle()来获取 SavedStateHandle 实例然后将其传入class CounterViewModel(private val application: Application, private val savedStateHandle: SavedStateHandle) : BaseViewModel() { companion object { val Factory: ViewModelProvider.Factory object : ViewModelProvider.Factory { Suppress(UNCHECKED_CAST) override fun T : ViewModel create(modelClass: ClassT, extras: CreationExtras): T { val application checkNotNull(extras[APPLICATION_KEY]) val savedStateHandle extras.createSavedStateHandle() return CounterViewModel(application, savedStateHandle) as T } } } …… }7.3. 结合可观察数据LiveData / StateFlowSavedStateHandle 不仅能存取普通值还能直接生成可观察数据流。这在和 UI 双向绑定时非常方便。LiveData版本class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: LiveDataListString savedStateHandle.getLiveDataString(query).switchMap { query - repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle[query] query } }这样即使系统杀掉进程用户重新打开时输入的查询条件也会恢复。StateFlow版本Lifecycle 2.5.0class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: StateFlowListString savedStateHandle.getStateFlow(query, ) .flatMapLatest { query - repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle[query] query } }与 LiveData 不同StateFlow 更适合配合 collect() 使用尤其是在 Compose 或协程场景中。Compose版本实验性Lifecycle 2.5.0 支持将 SavedStateHandle 与 Compose 的 rememberSaveable 互通class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: ListString by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData query } } }8. 总结ViewModel 是 MVVM 架构的核心之一它解决了传统 Android 开发中界面状态易丢失业务逻辑分散Activity/Fragment 过度臃肿等问题。通过 ViewModel我们可以轻松实现状态持久生命周期安全架构清晰数据驱动 UI。配合 LiveData、StateFlow、DataBinding 或 Compose即可构建出稳定、简洁、可维护的现代 Android 应用。更多详细的 ViewModel 介绍请访问 Android 开发者官网。