1. 背景

在应用开发中,随着业务逻辑的复杂化,我们往往需要管理越来越多的数据状态,例如网络请求结果、界面输入内容、滚动位置、用户交互状态等。这些数据通常会随着 Activity 或 Fragment 的生命周期而变化,如果处理不当,就容易出现数据丢失、界面闪烁、甚至内存泄漏等问题。

为了解决这些痛点,我们先简单回顾一下常见的应用架构演进过程。

MVC(Model-View-Controller)

MVC 是最早期的一种架构设计,View 负责界面展示,Model 负责数据处理,Controller 负责协调二者。

但在 Android 中,Activity/Fragment 通常既充当 View,又承担 Controller 的部分逻辑,结果导致代码臃肿,UI 与业务逻辑高度耦合。

MVP(Model-View-Presenter)

MVP 在 Android 中得到了广泛应用,Presenter 负责业务逻辑,View 只负责渲染界面。

虽然一定程度上解决了 Activity 的臃肿问题,但 Presenter 与 View 仍存在强引用关系,生命周期绑定困难。

例如当 Activity 重建(屏幕旋转)时,Presenter 需要重新创建、重新绑定,状态恢复变得繁琐。

MVVM(Model-View-ViewModel)

MVVM 架构中引入了 ViewModel,它从视图层中抽离出“可感知生命周期的状态持有者”,通过数据驱动(通常搭配 LiveData)来自动更新 UI。这让 UI 逻辑更轻,数据与生命周期解耦,成为现代 Android 推荐的架构模式之一。

2. 了解 ViewModel

2.1 ViewModel 是什么

ViewModel 是 Android Jetpack 架构组件中的核心之一,它是一个专为保存和管理界面状态(UI State) 而设计的类。它与 Activity 或 Fragment 的生命周期绑定,但可以在配置变化(如屏幕旋转)时保持数据不丢失。

简单来说:ViewModel = UI 状态缓存器 + 业务逻辑中枢。

2.2 ViewModel 的作用与优势

1. 状态持久性,保存界面相关数据,避免因配置变化导致丢失。

2. 业务逻辑集中,将业务逻辑与界面解耦,Activity 仅负责展示,提升架构清晰度与可测试性。

3. 生命周期感知,不用持有 View 引用,可与 LiveData 搭配实现数据驱动 UI

3. 实现 ViewModel

下面通过一个简单的示例来演示如何在项目中使用 ViewModel。

3.1. 定义 ViewModel 类

class CounterViewModel : ViewModel() {
    private var _count = MutableLiveData(0)
    val count: LiveData<Int> 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 = findViewById<TextView>(R.id.text_count)
        val buttonAdd = findViewById<Button>(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 version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="viewModel"
            type="com.zyx.viewmodel.demo.CounterViewModel" />
    </data>
    <LinearLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingTop="120dp"
        tools:context=".MainActivity">
        <TextView
            android:id="@+id/text_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`Count:` + String.valueOf(viewModel.count)}"
            android:textSize="20sp" />
        <Button
            android:id="@+id/button_add"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.increase()}"
            android:text="增加计数" />
    </LinearLayout>
</layout>

Activity 绑定:

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() (Compose 版是 viewModel())。 系统会自动根据当前的 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.ComponentActivity(Activity 场景)与 androidx.fragment.app.Fragment(Fragment 场景)中,若找不到类,可手动添加依赖:

dependencies {
    implementation "androidx.activity:activity-ktx:1.9.2"
    implementation "androidx.fragment:fragment-ktx:1.8.2"
}

4.2. 理解作用域

ViewModel 的生命周期依附于实现了 ViewModelStoreOwner 的组件(如 Activity 或 Fragment),而非其视图。
作用域决定了:

  1. 数据能保存多久;
  2. 是否能在多个界面共享 ViewModel 实例。

4.3. ViewModelStoreOwner

ViewModelStoreOwner 是一个接口,定义了一个组件是否能“拥有” ViewModel。Activity 和 Fragment 都实现了它,因此都可以作为 ViewModel 的持有者。

通常情况下,我们获取的 ViewModel 都限定在最近的 ViewModelStoreOwner 上。

例如:

  1. 在 Activity 中使用 by viewModels(),则作用域限定为该 Activity;
  2. 在 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. 协程作用域 viewModelScope

ViewModel 自带一个 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 的生命周期结束时清除 ViewModel,ViewModel 也会取消 CoroutineScope。

class CounterViewModel(private val viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)) : ViewModel() {
    private var _count = MutableLiveData(0)
    val count: LiveData<Int> 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: LiveData<Int> 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(Class<T>, 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: Class<T>, 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.Key<CounterRepository> {}
        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.get<String>("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 / ArrayList

Size / SizeF(API 21+)

对于自定义类,建议使用 @Parcelize 注解实现 Parcelable。

如果对象既不是 Parcelable 也不是 Serializable,可以自己注册保存逻辑:

savedStateHandle.setSavedStateProvider("key") {
    Bundle().apply {
        putString("path", file.absolutePath)
    }
}

系统保存状态时会自动调用这个 provider 的 saveState() 方法。
重新创建后,你可以通过 savedStateHandle.get<Bundle>("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.get<Bundle>("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 对象。

然后常一样创建 ViewModel

class 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: Class<T>, extras: CreationExtras): T {
                val application = checkNotNull(extras[APPLICATION_KEY])
                val savedStateHandle = extras.createSavedStateHandle()
                return CounterViewModel(application, savedStateHandle) as T
            }
        }
    }
    ……
}

7.3. 结合可观察数据(LiveData / StateFlow)

SavedStateHandle 不仅能存取普通值,还能直接生成可观察数据流。这在和 UI 双向绑定时非常方便。

LiveData 版本

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: LiveData<List<String>> =
        savedStateHandle.getLiveData<String>("query").switchMap { query ->
            repository.getFilteredData(query)
        }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

这样,即使系统杀掉进程,用户重新打开时输入的查询条件也会恢复。

StateFlow 版本(Lifecycle 2.5.0+)

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: StateFlow<List<String>> =
        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: List<String> by savedStateHandle.saveable {
        mutableStateOf(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

8. 总结

ViewModel 是 MVVM 架构的核心之一,它解决了传统 Android 开发中:

  1. 界面状态易丢失;
  2. 业务逻辑分散;
  3. Activity/Fragment 过度臃肿等问题。

通过 ViewModel,我们可以轻松实现:

  1. 状态持久;
  2. 生命周期安全;
  3. 架构清晰
  4. 数据驱动 UI。

配合 LiveData、StateFlow、DataBinding 或 Compose,即可构建出稳定、简洁、可维护的现代 Android 应用。

更多详细的 ViewModel 介绍,请访问 Android 开发者官网

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐