零基础也能搞懂的移动应用MVVM架构实战指南
写在前面的话:作为一个文科转码的过来人,我当初学架构模式的时候,被MVC、MVP、MVVM这些缩写搞得头都大了。后来我发现,其实MVVM没那么玄乎,今天就用大白话带你搞明白它,并且手把手教你在实战中用起来。
一、MVVM到底是个啥?
在讲MVVM之前,我们先聊聊为什么需要"架构"这个东西。
1.1 没有架构的代码有多可怕
我当初刚学Android的时候,写过这样一个页面:一个登录界面。代码大概长这样:
public class LoginActivity extends AppCompatActivity {
private EditText etUsername;
private EditText etPassword;
private Button btnLogin;
private TextView tvResult;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
etUsername = findViewById(R.id.et_username);
etPassword = findViewById(R.id.et_password);
btnLogin = findViewById(R.id.btn_login);
tvResult = findViewById(R.id.tv_result);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取输入
String username = etUsername.getText().toString();
String password = etPassword.getText().toString();
// 校验逻辑
if (username.isEmpty()) {
tvResult.setText("用户名不能为空");
return;
}
if (password.isEmpty()) {
tvResult.setText("密码不能为空");
return;
}
if (password.length() < 6) {
tvResult.setText("密码至少6位");
return;
}
// 网络请求
btnLogin.setEnabled(false);
tvResult.setText("登录中...");
new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟网络请求
Thread.sleep(2000);
boolean success = username.equals("admin")
&& password.equals("123456");
runOnUiThread(new Runnable() {
@Override
public void run() {
btnLogin.setEnabled(true);
if (success) {
tvResult.setText("登录成功!");
} else {
tvResult.setText("用户名或密码错误");
}
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
});
}
}
看起来能跑对吧?但问题很大:
- 所有逻辑都塞在Activity里,代码又臭又长
- 无法单元测试,因为逻辑和UI耦合在一起
- 难以复用,如果另一个页面也要登录校验,你得复制粘贴
- 难以维护,需求一改,整个Activity都要动
这就是为什么我们需要架构模式。
1.2 MVVM的三个字母分别代表什么
MVVM是三个单词的缩写:
| 缩写 | 全称 | 中文 | 通俗理解 |
|---|---|---|---|
| M | Model | 模型 | 数据层,管数据的存取和业务逻辑 |
| V | View | 视图 | 界面层,只管长什么样、怎么展示 |
| VM | ViewModel | 视图模型 | 中间人,连接数据和界面 |
用一句话概括:View负责展示,Model负责数据,ViewModel负责把数据变成View能用的样子。
1.3 一个生活化的比喻
想象你在餐厅吃饭:
- View(视图) = 你面前的餐桌和菜单展示牌。它只负责把菜端上来给你看,不负责做菜。
- Model(模型) = 厨房里的食材仓库和厨师。它负责准备食材、做菜,但不管菜怎么摆盘。
- ViewModel(视图模型) = 传菜员。他从厨房拿到菜,摆好盘,端到餐桌上。你不需要知道厨房在哪,传菜员会帮你搞定一切。
关键来了:传菜员(ViewModel)知道厨房(Model)在哪,也知道自己要把菜端到哪张桌子(View),但桌子(View)不需要知道厨房(Model)的存在。
这就是MVVM的核心思想——解耦。
二、技术选型:为什么选这套方案?
做移动开发,不同平台有不同的技术栈。我们先做个对比,然后选定一套来实战。
2.1 Android端MVVM技术栈对比
| 方案 | ViewModel实现 | 数据绑定 | 学习曲线 | 官方推荐度 |
|---|---|---|---|---|
| 传统MVVM | ViewModel + LiveData | DataBinding | 中等 | ⭐⭐⭐ |
| Jetpack MVVM | ViewModel + LiveData/StateFlow | ViewBinding | 较低 | ⭐⭐⭐⭐⭐ |
| MVI模式 | ViewModel + StateFlow | ViewBinding | 较高 | ⭐⭐⭐⭐ |
我的选择:Jetpack MVVM方案。 理由如下:
- Google官方推荐,生态最好
- 学习曲线相对平缓,适合新手
- 社区资源丰富,遇到问题容易找到答案
2.2 iOS端MVVM技术栈对比
| 方案 | 数据绑定方式 | 响应式框架 | 适用场景 |
|---|---|---|---|
| 原生MVVM | KVO/Delegate | Combine | iOS 13+ |
| RxSwift MVVM | 数据绑定 | RxSwift | 复杂项目 |
| SwiftUI MVVM | @Published/@ObservedObject | 原生 | 新项目首选 |
2.3 跨平台方案对比
| 框架 | 语言 | MVVM支持度 | 性能 | 生态 |
|---|---|---|---|---|
| Flutter | Dart | 原生支持 | 优秀 | 快速增长 |
| React Native | JavaScript | 需自行实现 | 良好 | 成熟 |
| Kotlin Multiplatform | Kotlin | 原生支持 | 优秀 | 发展中 |
本教程我们以Android Jetpack MVVM为主线来讲解,因为它的概念最清晰,最适合理解MVVM的本质。学完之后,你迁移到iOS或跨平台框架也会很轻松。
三、环境准备
3.1 你需要安装什么
| 工具 | 版本要求 | 用途 |
|---|---|---|
| Android Studio | Hedgehog (2023.1.1) 或更新 | 开发IDE |
| JDK | 17 | Java运行环境 |
| Android SDK | API 24+ | 安卓开发包 |
| Gradle | 8.0+ | 构建工具 |
3.2 创建项目
打开Android Studio → New Project → 选择 Empty Activity → 配置如下:
Name: MvvmDemo
Package name: com.example.mvvmdemo
Language: Kotlin
Minimum SDK: API 24
Build configuration language: Kotlin DSL (build.gradle.kts)
3.3 添加依赖
打开 app/build.gradle.kts,添加以下依赖:
dependencies {
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
// LiveData
implementation("androidx.lifecycle:livedata-ktx:2.7.0")
// Lifecycle (让ViewModel感知生命周期)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
// Coroutines (协程,用于异步操作)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// ViewBinding (替代findViewById)
// 在build.gradle.kts的android块中添加:
// buildFeatures {
// viewBinding = true
// }
}
别忘了在 android {} 块中开启ViewBinding:
android {
// ... 其他配置
buildFeatures {
viewBinding = true
}
}
四、核心概念详解
4.1 ViewModel:数据的管家
ViewModel的核心职责是持有和管理UI相关的数据,并且在配置变更(比如屏幕旋转)时存活下来。
class MyViewModel : ViewModel() {
// 持有数据
private val _userName = MutableLiveData<String>()
val userName: LiveData<String> = _userName
// 执行业务逻辑
fun loadUser() {
_userName.value = "张三"
}
}
我当初学的时候,最大的困惑是:为什么要搞一个带下划线的 _userName 和一个不带下划线的 userName?
答案是:封装。_userName 是私有的,只有ViewModel内部能修改它;userName 是公开的,外部只能观察它,不能修改它。这就防止了View随意篡改数据。
4.2 LiveData:会通知的数据
LiveData是一种可观察的数据容器。当数据变化时,它会自动通知所有观察者。
// 观察数据变化
viewModel.userName.observe(this) { name ->
// 每当userName变化,这里就会自动执行
textView.text = name
}
LiveData有三个关键特性:
- 生命周期感知:只在Activity处于活跃状态(STARTED或RESUMED)时才更新UI
- 防内存泄漏:Activity销毁时自动取消订阅
- 防空指针:Activity不在前台时不会触发回调
4.3 ViewBinding:告别findViewById
以前找控件:
val textView = findViewById<TextView>(R.id.tv_name)
现在用ViewBinding:
// 自动生成binding类
val binding = ActivityMainBinding.inflate(layoutInflater)
binding.tvName.text = "Hello"
更安全、更简洁、编译时就能发现错误。
五、实战项目:做一个待办事项App
光说概念太抽象,我们直接动手做一个完整的待办事项(Todo)应用。
5.1 项目结构规划
com.example.mvvmdemo/
├── model/
│ └── Todo.kt # 数据模型
├── repository/
│ └── TodoRepository.kt # 数据仓库
├── viewmodel/
│ └── TodoViewModel.kt # ViewModel
├── ui/
│ ├── MainActivity.kt # 主界面
│ └── adapter/
│ └── TodoAdapter.kt # 列表适配器
└── util/
└── Result.kt # 通用结果封装
5.2 第一步:定义数据模型(Model层)
// model/Todo.kt
data class Todo(
val id: Int,
val title: String,
val isCompleted: Boolean = false
)
就这么简单。Model层只定义数据结构,不包含任何UI逻辑。
5.3 第二步:创建数据仓库(Repository层)
Repository是Model和ViewModel之间的桥梁。我当初学的时候不理解为什么要多这一层,后来明白了:它是为了让ViewModel不关心数据到底从哪来。
// repository/TodoRepository.kt
class TodoRepository {
// 模拟本地数据
private val todoList = mutableListOf(
Todo(1, "学习MVVM架构", false),
Todo(2, "写一个Todo App", false),
Todo(3, "买牛奶", true)
)
// 获取所有待办
fun getAllTodos(): List<Todo> {
return todoList.toList()
}
// 添加待办
fun addTodo(title: String): Result<Todo> {
if (title.isBlank()) {
return Result.Error("标题不能为空")
}
val newTodo = Todo(
id = (todoList.maxOfOrNull { it.id } ?: 0) + 1,
title = title
)
todoList.add(newTodo)
return Result.Success(newTodo)
}
// 切换完成状态
fun toggleTodo(todoId: Int): Result<Todo> {
val index = todoList.indexOfFirst { it.id == todoId }
if (index == -1) {
return Result.Error("找不到该待办")
}
val todo = todoList[index]
val updatedTodo = todo.copy(isCompleted = !todo.isCompleted)
todoList[index] = updatedTodo
return Result.Success(updatedTodo)
}
// 删除待办
fun deleteTodo(todoId: Int): Result<Unit> {
val removed = todoList.removeAll { it.id == todoId }
return if (removed) {
Result.Success(Unit)
} else {
Result.Error("删除失败")
}
}
}
通用的Result封装:
// util/Result.kt
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
val isSuccess get() = this is Success
val isError get() = this is Error
fun getOrNull(): T? = (this as? Success)?.data
fun getErrorMessage(): String? = (this as? Error)?.message
}
5.4 第三步:创建ViewModel
这是整个MVVM的核心。ViewModel负责从Repository获取数据,并把它转换成View能直接使用的格式。
// viewmodel/TodoViewModel.kt
class TodoViewModel : ViewModel() {
private val repository = TodoRepository()
// ========== 对外暴露的LiveData ==========
// 待办列表(View直接观察这个)
private val _todos = MutableLiveData<List<Todo>>()
val todos: LiveData<List<Todo>> = _todos
// 加载状态
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
// 错误/提示信息
private val _message = MutableLiveData<String?>()
val message: LiveData<String?> = _message
// 统计数据(派生数据)
val todoCount: LiveData<Int> = _todos.map { list -> list.size }
val completedCount: LiveData<Int> = _todos.map { list ->
list.count { it.isCompleted }
}
// ========== 初始化 ==========
init {
loadTodos()
}
// ========== 业务方法 ==========
fun loadTodos() {
_isLoading.value = true
viewModelScope.launch {
// 模拟网络延迟
delay(500)
val todos = repository.getAllTodos()
_todos.value = todos
_isLoading.value = false
}
}
fun addTodo(title: String) {
if (title.isBlank()) {
_message.value = "请输入待办内容"
return
}
viewModelScope.launch {
val result = repository.addTodo(title)
when (result) {
is Result.Success -> {
loadTodos() // 重新加载列表
_message.value = "添加成功"
}
is Result.Error -> {
_message.value = result.message
}
}
}
}
fun toggleTodo(todoId: Int) {
viewModelScope.launch {
val result = repository.toggleTodo(todoId)
when (result) {
is Result.Success -> {
loadTodos()
}
is Result.Error -> {
_message.value = result.message
}
}
}
}
fun deleteTodo(todoId: Int) {
viewModelScope.launch {
val result = repository.deleteTodo(todoId)
when (result) {
is Result.Success -> {
loadTodos()
_message.value = "已删除"
}
is Result.Error -> {
_message.value = result.message
}
}
}
}
fun clearMessage() {
_message.value = null
}
}
注意看这段代码的设计思路:
- 私有可变 + 公开不可变:
_todos是私有的MutableLiveData,todos是公开的LiveData。View只能看,不能改。 - 派生数据:
todoCount和completedCount是从_todos自动计算出来的,不需要手动维护。 - 所有业务逻辑都在ViewModel里:View不需要知道Repository的存在。
5.5 第四步:创建列表适配器
// ui/adapter/TodoAdapter.kt
class TodoAdapter(
private val onToggle: (Int) -> Unit,
private val onDelete: (Int) -> Unit
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
private val todoList = mutableListOf<Todo>()
fun submitList(newList: List<Todo>) {
todoList.clear()
todoList.addAll(newList)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val binding = ItemTodoBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return TodoViewHolder(binding)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
holder.bind(todoList[position])
}
override fun getItemCount() = todoList.size
inner class TodoViewHolder(
private val binding: ItemTodoBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(todo: Todo) {
binding.tvTitle.text = todo.title
binding.tvTitle.paintFlags = if (todo.isCompleted) {
binding.tvTitle.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
binding.tvTitle.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
binding.cbDone.isChecked = todo.isCompleted
binding.cbDone.setOnClickListener {
onToggle(todo.id)
}
binding.btnDelete.setOnClickListener {
onDelete(todo.id)
}
}
}
}
5.6 第五步:布局文件
主界面布局 activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- 顶部统计栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvStats"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="共0项,已完成0项"
android:textSize="14sp" />
</LinearLayout>
<!-- 添加待办区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<EditText
android:id="@+id/etInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="输入新的待办事项" />
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加" />
</LinearLayout>
<!-- 加载指示器 -->
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<!-- 待办列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTodos"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
列表项布局 item_todo.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp">
<CheckBox
android:id="@+id/cbDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:textSize="16sp" />
<ImageButton
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_delete"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="删除" />
</LinearLayout>
5.7 第六步:在Activity中组装一切(View层)
// ui/MainActivity.kt
class MainActivity : AppCompatActivity() {
// 使用by viewModels()委托创建ViewModel
private val viewModel: TodoViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
private lateinit var adapter: TodoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupRecyclerView()
setupListeners()
observeViewModel()
}
private fun setupRecyclerView() {
adapter = TodoAdapter(
onToggle = { todoId -> viewModel.toggleTodo(todoId) },
onDelete = { todoId -> viewModel.deleteTodo(todoId) }
)
binding.rvTodos.layoutManager = LinearLayoutManager(this)
binding.rvTodos.adapter = adapter
}
private fun setupListeners() {
binding.btnAdd.setOnClickListener {
val input = binding.etInput.text.toString()
viewModel.addTodo(input)
binding.etInput.text.clear()
}
}
private fun observeViewModel() {
// 观察待办列表
viewModel.todos.observe(this) { todos ->
adapter.submitList(todos)
}
// 观察加载状态
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
// 观察统计信息
viewModel.todoCount.observe(this) { total ->
updateStats(total, viewModel.completedCount.value ?: 0)
}
viewModel.completedCount.observe(this) { completed ->
updateStats(viewModel.todoCount.value ?: 0, completed)
}
// 观察提示信息
viewModel.message.observe(this) { message ->
message?.let {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
viewModel.clearMessage()
}
}
}
private fun updateStats(total: Int, completed: Int) {
binding.tvStats.text = "共${total}项,已完成${completed}项"
}
}
看到没有?Activity里的代码非常干净! 它只做三件事:
- 设置UI(RecyclerView、按钮点击)
- 观察ViewModel的数据变化
- 把用户操作转发给ViewModel
Activity完全不需要知道数据从哪来、怎么存储、怎么校验。这就是MVVM的魅力。
5.8 数据流向总结
让我们用文字流程图梳理一下整个数据流:
用户点击"添加"按钮
↓
Activity 调用 viewModel.addTodo(input)
↓
ViewModel 校验输入 → 调用 repository.addTodo(title)
↓
Repository 操作数据 → 返回 Result
↓
ViewModel 收到 Result → 更新 _todos LiveData
↓
LiveData 通知所有观察者
↓
Activity 的 observe 回调触发 → 更新 RecyclerView
整个过程中,View(Activity)和Model(Repository)没有任何直接交互。 它们都只和ViewModel打交道。
六、进阶:MVVM中的常见模式
6.1 单事件处理(SingleLiveEvent)
上面的代码有个小问题:message 这个LiveData在屏幕旋转后会重新触发,导致Toast重复弹出。解决方案是使用SingleLiveEvent:
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
@MainThread
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
}
然后把ViewModel中的 _message 改成:
private val _message = SingleLiveEvent<String>()
val message: LiveData<String> = _message
6.2 使用StateFlow替代LiveData(更现代的方案)
如果你用的是比较新的项目,可以考虑用Kotlin的StateFlow:
| 特性 | LiveData | StateFlow |
|---|---|---|
| 生命周期感知 | ✅ 原生支持 | ❌ 需要手动处理 |
| 初始值 | ❌ 需要手动设置 | ✅ 构造函数必须提供 |
| 线程安全 | ✅ | ✅ |
| Kotlin协程集成 | 一般 | 原生支持 |
| 操作符 | 有限 | 丰富的Flow操作符 |
StateFlow版本的ViewModel:
class TodoViewModelV2 : ViewModel() {
private val repository = TodoRepository()
private val _todos = MutableStateFlow<List<Todo>>(emptyList())
val todos: StateFlow<List<Todo>> = _todos.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun loadTodos() {
viewModelScope.launch {
_isLoading.value = true
delay(500)
_todos.value = repository.getAllTodos()
_isLoading.value = false
}
}
}
在Activity中观察:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.todos.collect { todos ->
adapter.submitList(todos)
}
}
}
6.3 多ViewModel协作
当页面复杂时,你可能需要多个ViewModel。比如一个页面既有列表又有筛选器:
class TodoListViewModel : ViewModel() { /* 管理列表数据 */ }
class TodoFilterViewModel : ViewModel() { /* 管理筛选条件 */ }
class MainActivity : AppCompatActivity() {
private val listViewModel: TodoListViewModel by viewModels()
private val filterViewModel: TodoFilterViewModel by viewModels()
}
如果两个ViewModel需要共享数据,可以用 ViewModelProvider.Factory 或者把它们注入到同一个 Scoped ViewModel 中。
七、新手常见问题解答
Q1:ViewModel和Activity重建有什么关系?
A: 当屏幕旋转时,Activity会被销毁并重建,但ViewModel不会。这就是为什么我们把数据放在ViewModel里——旋转屏幕后数据不会丢失。
// 这段代码在屏幕旋转后,count的值依然保留
class CounterViewModel : ViewModel() {
val count = MutableLiveData(0)
fun increment() {
count.value = (count.value ?: 0) + 1
}
}
Q2:为什么不在Activity里直接写业务逻辑?
A: 我当初也有这个疑问。直到我遇到了这几个场景:
- 屏幕旋转:Activity重建,逻辑全丢了
- 单元测试:Activity依赖Android框架,没法在JVM上直接测试
- 代码复用:如果Fragment也要用同样的逻辑,你怎么办?
把逻辑放到ViewModel里,这些问题全部解决。
Q3:Repository层是不是多余的?
A: 对于小项目来说,确实可以直接在ViewModel里操作数据。但Repository的价值在于:
- 数据源切换透明:今天用本地数据库,明天换成网络API,ViewModel一行代码都不用改
- 缓存策略集中管理:先读缓存还是先读网络,在Repository里统一处理
- 可测试性:可以给ViewModel注入一个假的Repository来测试
Q4:MVVM和MVC有什么区别?
A: 简单对比:
| 特性 | MVC | MVVM |
|---|---|---|
| Controller/ViewModel | 持有View引用,主动更新UI | 不持有View引用,通过数据绑定被动更新 |
| View | 知道Controller存在 | 不知道ViewModel存在(理想状态) |
| 数据流向 | 双向,容易混乱 | 单向,清晰可追踪 |
| 可测试性 | 差(Controller依赖View) | 好(ViewModel不依赖View) |
Q5:什么时候该用MVVM,什么时候不用?
A:
- 用MVVM:中大型项目、需要单元测试、多人协作、数据逻辑复杂
- 不用MVVM:简单的工具类App、纯展示页面、原型验证阶段
不过话说回来,即使是很小的项目,养成MVVM的习惯也没坏处。
八、避坑指南
根据我当初踩过的坑,给你总结几个注意事项:
坑1:在ViewModel中持有Activity引用
// ❌ 错误示范
class MyViewModel(private val activity: MainActivity) : ViewModel() {
// 这会导致内存泄漏!Activity重建后ViewModel还持有旧的引用
}
// ✅ 正确做法
class MyViewModel : ViewModel() {
// 通过LiveData/StateFlow通知View,不直接操作View
}
坑2:在ViewModel中使用CoroutineScope
// ❌ 错误示范
class MyViewModel : ViewModel() {
private val scope = CoroutineScope(Dispatchers.Main)
// 这个scope不会随ViewModel销毁而取消,会内存泄漏
}
// ✅ 正确做法
class MyViewModel : ViewModel() {
fun doSomething() {
viewModelScope.launch {
// viewModelScope会自动在ViewModel销毁时取消
}
}
}
坑3:LiveData的setValue和postValue搞混
// 在主线程中
liveData.value = "新值" // ✅ 正确
liveData.postValue("新值") // ✅ 也可以,但没必要
// 在子线程中
liveData.value = "新值" // ❌ 崩溃!
liveData.postValue("新值") // ✅ 正确
坑4:过度设计
// ❌ 过度设计:一个简单的计数器页面搞了5层架构
// ✅ 适度设计:根据项目复杂度选择合适的架构层级
记住:架构是为你服务的,不是让你伺候它的。
九、学习建议:下一步怎么走?
恭喜你读到这里!你已经掌握了MVVM的核心概念和基本实战。接下来建议这样进阶:
第一阶段:巩固基础(1-2周)
- 把本教程的Todo App自己从零敲一遍
- 给App增加功能:编辑待办、分类筛选、搜索
- 尝试写单元测试(用
androidx.arch.core:core-testing)
第二阶段:引入真实数据源(2-3周)
- 把Repository对接真实的网络API(用Retrofit)
- 加入本地数据库缓存(用Room)
- 实现"先显示缓存,再刷新网络"的策略
第三阶段:学习依赖注入(2周)
- 学习Hilt或Koin
- 把Repository注入到ViewModel中,而不是直接new
- 理解依赖注入对可测试性的帮助
第四阶段:探索更现代的架构(持续学习)
- 学习MVI模式(Model-View-Intent)
- 了解Unidirectional Data Flow(单向数据流)
- 研究Compose中的MVVM(Jetpack Compose是未来方向)
推荐学习资源
| 资源 | 类型 | 推荐理由 |
|---|---|---|
| Google官方架构指南 | 文档 | 最权威 |
| "现在架构"系列文章 | 博客 | 中文社区优质内容 |
| Android Developers YouTube | 视频 | Google官方频道 |
| Now in Android示例项目 | 代码 | Google官方示例 |
十、总结
让我们回顾一下今天学到的内容:
- MVVM是什么:Model管数据,View管展示,ViewModel做中间人
- 为什么需要MVVM:解耦、可测试、可维护、可复用
- 怎么实现MVVM:ViewModel + LiveData/StateFlow + Repository
- 实战要点:
- ViewModel不持有View引用
- 数据单向流动
- 使用viewModelScope管理协程
- Repository隔离数据源
我当初学架构的时候,最大的感悟是:架构不是银弹,它不能帮你解决所有问题。但好的架构能让你在需求变更时不那么痛苦,在团队协作时不那么心累,在写测试时不那么想哭。
MVVM不是唯一的选择,但它是一个非常好的起点。当你真正理解了MVVM背后的思想——关注点分离和单向数据流——你会发现,学其他架构模式也变得容易了。
动手写代码吧!光看不练是学不会的。把这篇教程的Todo App敲一遍,然后加上你自己的功能,遇到问题再来回头看,你会有新的理解。
祝你学习愉快,转码路上不孤单!


评论 0