Android 中的 MVVM 是如何构建起来的

961 阅读22分钟

本系列为小说《逆袭西二旗》的技术讲解,用于详细说明剧情里涉及的开发细节。

ViewBinding 有什么优势

00.png

ViewBinding 是 Android 中用于简化访问布局中 View 过程的一项能力。

它避免手动多次调用 findViewById(),并以更类型安全的方式获取视图,从而减少样板代码并降低运行时错误风险。

面试一问

与 findViewById() 相比,ViewBinding 在类型安全与空安全上如何改进?

带来的收益是什么?

工作方式

在搞明白上面的问题之前,我们先来看看它的工作方式。

在项目中启用 ViewBinding 后,Android 会为每个 XML 布局文件生成一个 binding 类。该类的命名来自布局文件名:下划线会转换为驼峰,并在结尾追加 Binding。

例如,布局为 activity_main.xml 时,生成类为 ActivityMainBinding。

binding 类包含布局中各视图的引用,你可以直接访问它们,而无需做类型强转或再调用 findViewById()。

// 在 Activity 中的使用示例
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.textView.text = "Hello, ViewBinding! 关注 RockByte 公众号呀!"
    }
}

上例中,ActivityMainBinding 对应 activity_main.xml 布局。inflate() 用于创建 binding 实例;binding.root 传给 setContentView() 以设置内容视图。

优势

  • Type Safety(类型安全): 直接访问视图、避免强制类型转换,减少类型不匹配带来的运行时问题。
  • Cleaner Code(代码更干净): 减少 findViewById() 等样板。
  • Null Safety(空安全): 对可空视图更友好,降低与可选 UI 交互时出错风险。
  • Performance(性能): 与 DataBinding 不同,ViewBinding 运行时额外开销很小,因为它不处理数据绑定表达式,也不做额外的绑定同步。

对比 DataBinding

DataBinding 能力更强,例如支持绑定表达式、双向数据绑定,但也更复杂并带来运行时成本。而 ViewBinding 更专注于让视图访问更简单、运行更轻。当你不需要更高级的数据驱动绑定时,它更合适。

启用 ViewBinding

在 build.gradle 中加入如下配置以启用 ViewBinding:

android {
    buildFeatures {
        viewBinding = true
    }
}

启用后,项目中的 XML 布局会对应自动生成 binding 类。

补充提示:
在 Google 官方正式支持 ViewBinding 之前,ButterKnife 很常用:通过注解处理将 View 注入到字段,从而绕开 findViewById,并借助类似依赖注入的方式提供类型安全。
它一度是 Android 生态中非常重要的库,也几乎是奠定 Jake Wharton 影响力的重要项目之一,并在 Android 开发里激发了大量实践与变化。
虽然随着 ViewBinding 被官方采纳,ButterKnife 已被标记弃用,但在当时它非常有创新性;对希望了解依赖注入模式的人来说,也仍然值得学习。

总结

ViewBinding 是一种更轻、类型更安全的视图交互方式,也能改善代码安全性。它可作为 findViewById() 的替代方案,当你不需要 DataBinding 的高级能力时,是很合适的选择。

启用 ViewBinding 能简化 UI 相关代码,并提升可维护性与运行时安全。

在使用 XML 布局时,我一定会启用 ViewBinding,反而是 DataBinding,我几乎不用。

DataBinding 是如何工作的

既然刚学完 ViewBinding,那么我们立即看看 DataBinding。

DataBinding 是 Android 提供的能力,让 XML 布局中的 UI 组件可以直接与数据源建立绑定。它通过减少 findViewById() 等样板,并在 UI 与数据模型间支持更实时的变化同步,为 UI 设计带来更偏“声明式”的写法。

此外,它也在 MVVM(Model-View-ViewModel)架构中扮演重要角色。

大家知道吗? MVVM 最初是由 Microsoft 作为分离 UI 逻辑与业务逻辑的模式提出的。

面试一问

DataBinding 与 ViewBinding 的关键差异是什么?如何取舍?

DataBinding 在 MVVM 中扮演什么角色?它如何帮助在 Android 开发里分离 UI 逻辑与业务逻辑?

启用 DataBinding

和 ViewBinding 类似,在 build.gradle 中加入如下配置以启用 DataBinding:

android {
    buildFeatures {
        dataBinding = true
    }
}

工作原理

DataBinding 会对使用 <layout> 的 XML 布局生成 binding 类。该类能直接提供视图访问,并允许在 XML 中使用表达式完成数据绑定。

下面展示一个典型的 DataBinding XML 布局示例:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="user"
            type="com.rockbyte.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.age}" />
    </LinearLayout>
</layout>

在这个例子里,一个 User 对象与 XML 布局绑定,并在 TextView 中显示 user.name 与 user.age。

在代码中绑定数据的示例:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        val user = User("Alice", 25)
        binding.user = user
    }
}

data class User(val name: String, val age: Int)

这里将 user 作为布局的数据源,用于完成初始展示;如果希望后续数据变化继续驱动 UI 更新,需要使用 LiveData、ObservableField、BaseObservable 等可观察数据源。

特性

  1. Two-Way Data Binding(双向数据绑定): 在 UI 与数据模型间自动保持同步,常用于表单、输入等场景。

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@={user.name}" />
    
  2. Binding Expressions(绑定表达式): 在 XML 中写简单逻辑,例如条件判断等。这确实是 DataBinding 强大功能的一个展现。

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.age > 18 ? `Adult` : `Minor`}" />
    
  3. Lifecycle Awareness(生命周期感知): 当结合 LiveData 并设置 lifecycleOwner 时,可以按生命周期状态观察数据并更新 UI。

优势

  • Reduces Boilerplate(减少样板): 减少 findViewById() 与大量手动更新 UI 的代码。
  • Real-Time UI Updates(更实时的 UI 更新): 可观察数据变化时,UI 能自动体现。
  • Declarative UI(声明式 UI): 将部分展示逻辑放到 XML 中,便于组织复杂布局。
  • Improves Testability(可测试性更好): UI 与代码解耦,有利于分别测试。

缺点

  • Performance Overhead(性能开销): 相比更轻量方案(如仅 ViewBinding)有额外运行时成本。
  • Complexity(复杂度): 小型/简单项目可能带来不必要复杂度。
  • Learning Curve(学习曲线): 需要理解绑定表达式与生命周期等问题。

总结

DataBinding 让你可以把 UI 元素在 XML 中直接绑定到数据源,减少样板,并以更偏声明式的方式管理 UI。

它支持双向绑定、表达式等能力,是动态、交互式界面的强工具。但也会带来复杂性与运行开销;当应用需要更实时、交互性更强的 UI 且希望减少手动控制时,它是很合适的选择。

进阶:ViewBinding 与 DataBinding

ViewBinding 与 DataBinding 都用于减少视图相关样板,但目标与能力集不同。下面是更细的对照说明。

ViewBinding 用于简化在布局中访问 View:为每个带 id 的视图生成更直接的引用,避免 findViewById,提升类型安全并减少重复代码。

ViewBinding 的关键特点:

  • 为各 XML 布局生成 binding 类。
  • 提供对布局中视图的直接引用,绕开 findViewById。
  • 通过编译期信息提升类型/空值方面的安全性(相对手写查找)。
  • 不自带绑定表达式/数据驱动更新等高级能力。

DataBinding 更复杂、也更强,它允许把 UI 与数据源在 XML 中绑定,支持绑定表达式、可观察数据、双向数据绑定,因此也更常见于 MVVM 等场景。

DataBinding 的关键特点:

  • 在 XML 中把 UI 与数据源建立绑定关系。
  • 通过表达式在 UI 上表达动态行为。
  • 提供双向数据绑定,让 UI 与数据更实时同步。
  • 可与 LiveData、ObservableField、BaseObservable 等结合做可观察的数据流。

它们之间存在如下的关键差异:

  1. 目的: ViewBinding 偏“更安全的取 View”;DataBinding 偏“数据驱动 UI 绑定与更新”。
  2. 生成内容: ViewBinding 主要生成视图引用;DataBinding 还会围绕数据生成额外绑定能力(不止视图引用本身)。
  3. 表达式: ViewBinding 不在 XML 中执行表达式;DataBinding 支持。
  4. Two-Way Data Binding(双向绑定): 只有 DataBinding 提供。
  5. 性能: 通常 ViewBinding 更轻,因为不跑数据绑定那条解析/同步链路。

简单来说。

需要简单取引用、不依赖 findViewById 时,用 ViewBinding(更偏“简单项目/场景”)。在复杂数据驱动 UI、或 MVVM 中需要与 LiveData / ObservableField / @Bindable 等结合时,用 DataBinding 更对口。

但 DataBinding 也带来额外成本;若 ViewBinding 已足够,就没必要为数据绑定多付那部分复杂度。

LiveData

LiveData 是 Jetpack 架构组件里提供的一种可观察数据容器。它具备生命周期感知能力,会关注与之关联的 LifecycleOwner(如 Activity、Fragment 或 viewLifecycleOwner)的生命周期,并倾向于只在组件处于活跃生命周期状态(如 started / resumed 等)时,才把更新投递给 UI,从而降低不合适时机更新 UI 带来的风险。

LiveData 的核心用途之一,是让 UI 能观察数据变化,并在数据变化时自动自我更新。这也是 Android 上实现更“响应式” UI 的重要工具之一。

LiveData 的一些优势包括:

  1. Lifecycle Awareness(生命周期感知): 在组件不活跃时减少不必要更新,有助于降低崩溃与泄漏风险等。
  2. Automatic Cleanup(自动清理): 与某个生命周期绑定的观察者在销毁时更易于被处理。
  3. Observer Pattern(观察者模式): 数据变化时,可通过观察者让 UI 自动跟随更新。
  4. Thread Safety(线程安全设计): 设计上也考虑到来自后台线程更新数据等需求。

以下示例展示如何在 ViewModel 中用 LiveData 管理 UI 相关数据:

// ViewModel
class MyViewModel : ViewModel() {
    private val _data = MutableLiveData<String>() // 内部可变的 MutableLiveData
    val data: LiveData<String> get() = _data      // 对外暴露为 LiveData,避免外部直接写入

    fun updateData(newValue: String) {
        _data.value = newValue // 更新 LiveData 值
    }
}

// Fragment 或 Activity
class MyFragment : Fragment() {
    private val viewModel: MyViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 观察 LiveData
        viewModel.data.observe(viewLifecycleOwner) { updatedData ->
            // 用新数据更新 UI
            textView.text = updatedData
        }
    }
}

上例中,MyViewModel 保存数据,Fragment 通过观察 LiveData 在 updateData() 被调用时自动刷新 UI。

上述代码中,你会看到 MutableLiveData 与 LiveData,这两者的差别其实很简单:

  • MutableLiveData: 通常可用 setValue() / postValue() 修改;常见实践是在 ViewModel 中内部持有可变版本,并对外用只读 LiveData 暴露,避免被外部直接改写。
  • LiveData(只读暴露): 更强调封装与避免外部非预期写入。

类似 MutableList 与 List 的差别。

面试一问

LiveData 如何体现生命周期感知?与 RxJava、EventBus 相比,优势在哪里?

setValue() 与 postValue() 在 LiveData 中有什么区别?各自更适用于什么时候?

LiveData 有哪些局限?当多个 UI 事件(导航、Toast 等)需要被观察,又不希望在配置变化后被重复触发时,你通常会怎么处理?

使用场景

  1. UI 状态管理: 把网络/数据库等来源数据放入 LiveData,与 UI 绑定,数据变化时界面同步更新。
  2. 观察者模式落地: 作为发布/订阅里的一方,在值变化时通知订阅方;适合更动态的 UI/交互数据。
  3. One-time events(一次性事件): 例如 Toast、跨页面导航等;这类场景也常见 SingleLiveEvent 等变体/封装,以避免重复触发等问题。

额外知识

LiveData 常会被拿来与 Kotlin 的 StateFlow、SharedFlow 对比,不少开发者会迁移到 Flow 或正考虑迁移。

有人会觉得 StateFlow 让 LiveData “过时了”,这句话从趋势上看有一定道理。不过有一点你需要搞清楚,Flow 是平台无关的,本身并不了解 Android 生命周期,因此收集与取消需要你在生命周期边界下显式处理。

没有可靠的生命周期感知式收集,Flow 可能在 UI 不活跃时仍继续收集,从而增加资源浪费甚至泄漏等风险。相比之下,LiveData 会绑定到 LifecycleOwner 并更自动地处理取消订阅,因此在不少场景下仍然更“省心”、也更实际。

另有一种说法是“从 LiveData 迁到 Flow 就能拿掉 Android 依赖”,这句话不一定站得住脚:不是所有人都有跨平台的需求。

在实践中,LiveData 往往只出现在 ViewModel 边界,是否要把网络/数据/领域层都换成更 JVM-only 的模块化架构,以及收益是否足够明确,也因人而异。

所以不必盲目追热点,当你真的需要 Flow,并且确实能拿到 Flow 的收益(更复杂数据转换、更丰富的响应式流处理等)时,再引入会更合适。

总结

LiveData 让构建 响应式 + 更生命周期安全 的 UI 状态更简单:用一种更“生命周期有感知”的方式观察数据变化,减少样板并降低在错误生命周期更新等带来的风险。它也因此成为现代 Android 开发、尤其是 MVVM 中的常用组件之一。

进阶:setValue 与 postValue

在 LiveData 中,两者都用于更新所持有的值,但它们的线程语义与使用场景不同。

setValue

setValue() 用于同步更新值,并且必须在主线程调用。当你希望立刻让观察者在本帧/同步路径里收到更新,它更合适。

val liveData = MutableLiveData<String>()

fun updateOnMainThread() {
    liveData.setValue("Updated Value") // 必须在主线程调用
}

这适合已经在主线程触发的更新(例如直接 UI 事件、生命周期回调内的同步更新等)。在后台线程调用 setValue() 通常会直接出错。

postValue

postValue() 用于在后台线程投递一个会在主线程生效的更新,从设计上更适合后台线程,避免在后台线程里手动切回主线程。

postValue() 不阻塞当前调用线程,而是把值投递到主线程去应用。

val liveData = MutableLiveData<String>()

fun updateInBackground() {
    Thread {
        liveData.postValue("Updated Value") // 可在任意线程调用
    }.start()
}

在涉及网络、数据库等后台工作时,postValue() 很常用,因为它不需要你显式再切主线程去更新 LiveData。

如果查看 postValue() 的内部实现,它通常会通过执行器/调度,把“在主线程上应用新值”这件事投递出去:

protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

它会通过 mDataLock 同步、更新 mPendingData,并判断是否需要向主线程投递任务;也解释了为何短时间内多次 postValue() 可能只让观察者看到“最后那次”的合并行为(以内部实现为准)。

很多开发者可能会使用 postValue() 去投递序列。现在你知道了吧,这个做法其实有隐患。

如果下面这段运行在主线程:

liveData.postValue("a")
liveData.setValue("b")

"b" 会立刻被设置,但之后主线程若仍处理到 postValue("a") 投递来的任务,最终值可能被覆盖为 "a"。原因是 postValue() 的异步调度与 setValue() 的同步更新交织。

关键差异

方面setValuepostValue
线程必须在主线程调用可在任意线程调用
同步/异步同步立即更新异步投递到主线程再更新
使用场景主线程上发起的更新(交互、生命周期等)后台线程/异步工作完成后要更新到 UI 侧时
观察者通知同帧/同步路径上更快触发主线程处理投递任务后触发(与帧调度相关)

用法建议

  • 用 setValue(): 更新已在主线程产生(用户交互、生命周期内同步结果等)。
  • 用 postValue(): 在后台线程拿到结果后,需要把值安全送到主线程侧(网络、DB、长任务等)。

总之。

setValue() 与 postValue() 都用于更新 LiveData 值,但线程模型不同:前者主线程同步,后者可跨线程并以投递方式落到主线程。选对了才能既线程安全,又让数据更新对 UI 更平滑。

ViewModel 是什么

11.png

Jetpack ViewModel 是 Android 架构组件中的关键能力之一,用于在与生命周期更匹配的边界内保存与管理 UI 相关数据。

这个架构组件有多重要?AndroidX 甚至在跨平台方向上也保留了 ViewModel。

它把 UI 侧逻辑与业务侧逻辑更清晰地分层,并尽量让数据在配置变化(如屏幕旋转)中得以保留,从而帮助应用更稳、更易于维护。

ViewModel 的核心目标之一,是在配置变化期间保留与 UI 强相关的数据:例如用户旋转设备导致 Activity / Fragment 重建时,ViewModel 仍可能保留关键状态,避免你重复拉取/重复计算。

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // 暴露屏幕 UI 状态
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // 处理业务逻辑
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

这个例子说明:即使 Activity 因配置变化重建,状态仍可能由 ViewModel 维持。

面试一问

ViewModel 如何在配置变化中“持久化”UI 相关数据?与 onSaveInstanceState() 等保存方式的核心差异是什么?

ViewModelStoreOwner 的用途是什么?同一 Activity 下多个 Fragment 间如何共享 ViewModel?

在 ViewModel 内用 StateFlow 或 LiveData 管理 UI 状态,各自优势与可能踩坑分别是什么?

特性

  1. Lifecycle Awareness(生命周期作用域): ViewModel 通常与某个 Activity / Fragment 的作用域绑定,并在对应 UI 组件不再需要时被清理(如用户离开页面)。
  2. Persistence Across Configuration Changes(跨配置变化保留状态): 与会在配置变化中销毁/重建的 Activity / Fragment 不同,ViewModel 往往更能保留其内部状态,减少丢数据/重复重取。
  3. Separation of Concerns(关注点分离): 把与 UI 展示强相关的状态/逻辑与业务逻辑分层,让 UI 侧观察/订阅 ViewModel 输出,更利于用响应式思路组织代码。

我们来看一下它的创建方式和使用示例。

在 ComponentActivity 上,可以用 Kotlin 的 by viewModels() 委托来创建 ViewModel(来自 activity-ktx 等 Jetpack 扩展能力),让代码更干净:

class DiceRollActivity : AppCompatActivity() {

    // 使用 `by viewModels()` Kotlin 属性委托
    // 来自 activity-ktx
    private val viewModel: DiceRollViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 系统第一次调用该 Activity 的 onCreate 时创建 ViewModel。
        // 配置变化导致 Activity 重建时,会拿到同一份 DiceRollViewModel 实例。

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // 更新 UI
                }
            }
        }
    }
}

ViewModel 实例会绑定在 ViewModelStoreOwner 上:这个 owner 可以是 Activity、Fragment、Navigation 图、图中某个 destination,甚至是自定义 owner。Jetpack 也提供了很丰富的“作用域化取 ViewModel”的方式。

图解 ViewModel

1.jpg

该图列出了 Jetpack 中与 ViewModel 相关的 API、返回实例的作用域,以及使用示例。并提示可将 ViewModel 挂到 ViewModelStoreOwner 上,owner 可以是 Activity、Fragment、Navigation 图/destination 或自定义 owner。

在 “Get an instance in Jetpack Compose” 中(lifecycle-viewmodel-compose)包含 viewModel()(取最近 ViewModelStoreOwner)与 viewModel(vmStoreOwner)(指定任意 ViewModelStoreOwner,示例含 Navigation backStackEntry / 父级图路由等)等要点;在 “Compose + Navigation + Hilt”(hilt-navigation-compose)中强调与 Hilt + Navigation 同用时可用 hiltViewModel()(并配套示例)。

在 “Core ViewModel API”(lifecycle-viewmodel-ktx)展示 ViewModelProvider 等。

在 “Get an instance in an Activity”(activity-ktx)展示 by viewModels()。

在 “Get an instance in a Fragment”(fragment-ktx)展示 by viewModels()、带 ownerProducer 的变体、以及 by activityViewModels();在 “Fragment + Navigation”(navigation-fragment)展示 by navGraphViewModels;在 “Fragment + Navigation + Hilt”(hilt-navigation-fragment)展示 by hiltNavGraphViewModels,并说明 Hilt + Navigation 场景下应优先使用这些 API,而非简单 navGraphViewModels;不挂在 NavGraph 的实例则仍可用 viewModels() / activityViewModels()。

该图内容较多,但是并不复杂,各位不必一次性看完。

总结

Jetpack ViewModel 用于管理 UI 相关数据,并尽量在配置变化中连续保留(例如屏幕旋转时不必因重建就把状态全丢回初始)。它是生命周期有感知的一类状态容器/协调者,也更常与 MVVM 等架构模式一起使用:通过保留状态与更清晰的边界,让状态管理与开发体验都更顺。

进阶:ViewModel 的生命周期

ViewModel 的生命周期与其 ViewModelStoreOwner 关联;这个 owner 可以是 Activity、Fragment,或其他与生命周期相关的承载者。

只要 ViewModelStoreOwner 仍在作用域中,ViewModel 就仍然存在,让数据与状态在配置变化(如屏幕旋转)中得以保留。

这种设计让 ViewModel 成为管理 UI 相关数据、并在此类变化中保持状态的重要组件。

例如,对 Activity 来说,ViewModel 往往持续到该 Activity 被真正结束并清理对应的 ViewModelStore。于是数据与状态也能够在配置变化期间被保留下来。

2.jpg

上图概览了 ViewModel 在 Activity 生命周期中的存续区间:它展示 ViewModel 如何随 Activity 的生命周期事件被创建并保留,即便 Activity 被暂时销毁后重建,实例仍可能延续。虽然示例是 Activity,但同样适用于 Fragment 以及作为 ViewModelStoreOwner 的其它承载者。

从图中看,左侧是 Activity 生命周期与关键节点:从 Activity created 开始,经 onCreate → onStart → onResume;Activity rotated 时经历 onPause → onStop → onDestroy,随后重建的 Activity 继续 onCreate → onStart → onResume;在更靠下的位置有 finish() 触发,并最终 onPause → onStop → onDestroy 以进入 Finished。右侧/中部是贯穿的 ViewModel Scope 区域,覆盖初次创建、旋转与重建;清理点以 onCleared() 为标志。

在 ViewModelStoreOwner 第一次被创建时,ViewModel 会被初始化。只要 owner 仍在内存中,通常会保留同一份 ViewModel 实例。

若发生配置变化(如旋转设备),owner 可能重建,但已存在的 ViewModel 往往被复用,避免重新加载或重新初始化数据。这种复用通常带来更好的性能与更顺滑的体验。

最后,当 ViewModelStoreOwner 被永久销毁时(例如 Activity 被 finish、或 Fragment 从父级中移除且预期不再回来),ViewModel 会被清理。

此时会调用其 onCleared(),用于取消协程、释放长期持有的资源等,以降低内存泄漏风险。ViewModel 的生命周期因此有助于在状态续存与资源管理之间取得平衡。

进阶:ViewModel 为什么会被保留

在 Android 中,Jetpack ViewModel 被设计为能挺过屏幕旋转、系统语言/区域变更等配置变化。

为某个 UI 组件(如 Activity/Fragment)创建 ViewModel 时,它会绑定到该组件提供的 ViewModelStoreOwner:对 Activity 来说,这个 owner 通常由 ComponentActivity 表示;对 Fragment 来说则是 Fragment 自身。

更底层的关键是 ViewModelStore:它会在配置变化中被保留,使 ViewModel 在界面重建时仍有机会保留其内部数据,而不必每次从零开始。

在 Jetpack 实现里,androidx.activity.ComponentActivity 与 androidx.fragment.app.Fragment 都实现了 ViewModelStoreOwner:让 Activity/Fragment 拥有自己的 ViewModelStore,以在配置变化中持续持有 ViewModel 实例。ViewModelStore 内部以 Map(key 为 String)管理实例,如:

public open class ViewModelStore {

    private val map = mutableMapOf<String, ViewModel>()

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun put(key: String, viewModel: ViewModel) {
        val oldViewModel = map.put(key, viewModel)
        oldViewModel?.clear()
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public operator fun get(key: String): ViewModel? {
        return map[key]
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun keys(): Set<String> {
        return HashSet(map.keys)
    }

    public fun clear() {
        for (vm in map.values) {
            vm.clear()
        }
        map.clear()
    }
}

ComponentActivity 还会观察自身生命周期:在 ON_DESTROY 时,会清理 Context 相关帮助器,并且若不是配置变化导致的销毁,会清理 ViewModelStore,以释放 ViewModel 实例:

getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            // 清理可用 context
            mContextAwareHelper.clearAvailableContext();
            // 并清理 ViewModelStore
            if (!isChangingConfigurations()) {
                getViewModelStore().clear();
            }
            mReportFullyDrawnExecutor.activityDestroyed();
        }
    }
});

总之。

ViewModel 之所以能在配置变化中“活下来”,与 ViewModelStore 的持有方式密切相关:由绑定生命周期的 Android 组件管理。

在 Compose Navigation 中,ViewModelStore 也会与导航路由/作用域产生关联,以按导航作用域保留状态。你当然也能手动管 ViewModelStore,但配置变化下正确“找回/复用”往往更绕,一般不建议自研乱改。

进阶:Jetpack 与 Microsoft

根据 Android 官方文档的表述,Jetpack 的 ViewModel 是一个生命周期有感知的组件,适合作为 业务逻辑 / 屏级状态 的承载体:对 UI 暴露状态,并封装与界面展示强相关的业务逻辑。

它的强项之一,是把状态缓存/延续在配置变化(如旋转、Activity 重建等)中,让 UI 不必无意义地重复取数/重复计算。总体上,它更“Android 化”、更围绕生命周期来组织状态。

而 MVVM(Model-View-ViewModel) 最初由 Microsoft 系统阐述时,其中的 ViewModel 更偏“View 与 Model 的桥梁”:实现可绑定到 View 的属性/命令,并通过通知机制把状态变化告知 View。

它要协调 View 对 Model 的访问,并尽量把业务逻辑从直接操控 UI 中抽离。与 Jetpack ViewModel 相比,MVVM 的 ViewModel 更强调数据绑定与被动 UI 这套机制,而不是 Android 上“过配置不丢”的那条线。

3.jpg

从图中可以看到,三个框为 View、ViewModel、Model。View 到 ViewModel 的实线箭头标为 Data Binding and Commands;ViewModel 到 Model 的实线箭头标为 ViewModel updates the model;Model 到 ViewModel、ViewModel 到 View 的虚线箭头标为 Send notifications。

在根本层面,二者同名但范式不同:Jetpack ViewModel 更强调 Android 上的生命周期有感知状态与跨配置续存;MVVM 模式里的 ViewModel 更强调 绑定/命令/解耦 以让 View 更被动。

名字相似不代表“只用它就等于 MVVM”。要更接近 MVVM 的原始目标,你往往还需要额外的数据绑定/命令体系,让 UI 能被动地响应 ViewModel 提供的数据,并把业务逻辑与 UI 层更清晰地分层,从而提高可测性与可维护性。

简单来说,ViewModel + Flow/LiveData 可以承担 MVVM 中 ViewModel 的一部分职责,但二者不能简单画等号。

总的来说。

二者范围与落点不同:Jetpack ViewModel 更服务 Android 上“UI 相关状态在生命周期/配置变化下如何更稳地组织”;MVVM 的 ViewModel 更服务“Model-View-ViewModel 之间如何用绑定把 UI 做声明式/被动化”。

在 Jetpack Compose 中,更常见的是直接从 ViewModel 观察流/状态来驱动 UI;在传统 XML 上,要更贴近经典 MVVM 的被动 UI,常还需要 DataBinding 等能力配合。

对于开发者来讲,你仍应按架构与 UI 技术栈选择更合适的组合。