零基础也能搞懂的移动应用MVVM架构实战指南

GC观察员
2026-07-24 13:55
阅读 631

写在前面的话:作为一个文科转码的过来人,我当初学架构模式的时候,被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方案。 理由如下:

  1. Google官方推荐,生态最好
  2. 学习曲线相对平缓,适合新手
  3. 社区资源丰富,遇到问题容易找到答案

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有三个关键特性:

  1. 生命周期感知:只在Activity处于活跃状态(STARTED或RESUMED)时才更新UI
  2. 防内存泄漏:Activity销毁时自动取消订阅
  3. 防空指针: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
    }
}

注意看这段代码的设计思路:

  1. 私有可变 + 公开不可变_todos 是私有的 MutableLiveDatatodos 是公开的 LiveData。View只能看,不能改。
  2. 派生数据todoCountcompletedCount 是从 _todos 自动计算出来的,不需要手动维护。
  3. 所有业务逻辑都在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里的代码非常干净! 它只做三件事:

  1. 设置UI(RecyclerView、按钮点击)
  2. 观察ViewModel的数据变化
  3. 把用户操作转发给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: 我当初也有这个疑问。直到我遇到了这几个场景:

  1. 屏幕旋转:Activity重建,逻辑全丢了
  2. 单元测试:Activity依赖Android框架,没法在JVM上直接测试
  3. 代码复用:如果Fragment也要用同样的逻辑,你怎么办?

把逻辑放到ViewModel里,这些问题全部解决。

Q3:Repository层是不是多余的?

A: 对于小项目来说,确实可以直接在ViewModel里操作数据。但Repository的价值在于:

  1. 数据源切换透明:今天用本地数据库,明天换成网络API,ViewModel一行代码都不用改
  2. 缓存策略集中管理:先读缓存还是先读网络,在Repository里统一处理
  3. 可测试性:可以给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周)

  1. 把本教程的Todo App自己从零敲一遍
  2. 给App增加功能:编辑待办、分类筛选、搜索
  3. 尝试写单元测试(用 androidx.arch.core:core-testing

第二阶段:引入真实数据源(2-3周)

  1. 把Repository对接真实的网络API(用Retrofit)
  2. 加入本地数据库缓存(用Room)
  3. 实现"先显示缓存,再刷新网络"的策略

第三阶段:学习依赖注入(2周)

  1. 学习Hilt或Koin
  2. 把Repository注入到ViewModel中,而不是直接new
  3. 理解依赖注入对可测试性的帮助

第四阶段:探索更现代的架构(持续学习)

  1. 学习MVI模式(Model-View-Intent)
  2. 了解Unidirectional Data Flow(单向数据流)
  3. 研究Compose中的MVVM(Jetpack Compose是未来方向)

推荐学习资源

资源 类型 推荐理由
Google官方架构指南 文档 最权威
"现在架构"系列文章 博客 中文社区优质内容
Android Developers YouTube 视频 Google官方频道
Now in Android示例项目 代码 Google官方示例

十、总结

让我们回顾一下今天学到的内容:

  1. MVVM是什么:Model管数据,View管展示,ViewModel做中间人
  2. 为什么需要MVVM:解耦、可测试、可维护、可复用
  3. 怎么实现MVVM:ViewModel + LiveData/StateFlow + Repository
  4. 实战要点
    • ViewModel不持有View引用
    • 数据单向流动
    • 使用viewModelScope管理协程
    • Repository隔离数据源

我当初学架构的时候,最大的感悟是:架构不是银弹,它不能帮你解决所有问题。但好的架构能让你在需求变更时不那么痛苦,在团队协作时不那么心累,在写测试时不那么想哭。

MVVM不是唯一的选择,但它是一个非常好的起点。当你真正理解了MVVM背后的思想——关注点分离单向数据流——你会发现,学其他架构模式也变得容易了。

动手写代码吧!光看不练是学不会的。把这篇教程的Todo App敲一遍,然后加上你自己的功能,遇到问题再来回头看,你会有新的理解。

祝你学习愉快,转码路上不孤单!

评论 0

最热最新
暂无评论
GC观察员Lv.1
0
影响力
0
文章
0
粉丝