第39章:MVVM架构:Model-View-ViewModel模式、Repository模式、数据绑定(DataBinding)

说实话,MVVM 这个词在 Android 圈子里已经被说烂了。但真正能把 MVVM 用明白、用舒服的,其实不多。我见过不少项目,说是 MVVM,结果 ViewModel 里塞满了网络请求代码,Activity 里直接操作数据库——嗯,那还不如用 MVC 呢。

这一章,咱们就聊聊 MVVM 的核心三件套:Model-View-ViewModelRepository 模式、以及 DataBinding。我会结合我自己的踩坑经历,帮你把这些概念落地到实际项目中。

1. MVVM 到底解决了什么问题?

先问个问题:你写 Android 的时候,最头疼的是什么?

我个人觉得,是状态管理。Activity 里一堆回调,数据一变就要手动更新 UI,稍不留神就空指针。更别说屏幕旋转了——那简直是噩梦。

MVVM 的核心思路很简单:把 UI 逻辑和业务逻辑彻底分开。ViewModel 不持有 View 的引用,View 只负责展示,ViewModel 只负责处理数据。这样,你的代码就能经得起屏幕旋转的考验。

核心原则:View 观察 ViewModel 的状态变化,ViewModel 通过 Repository 获取数据,Model 就是纯数据实体。

我在项目中遇到过一种情况:一个同事把网络请求直接写在 Activity 里,结果每次旋转屏幕都要重新请求。换成 MVVM 后,ViewModel 存活在旋转过程中,数据一次请求,终身受用。

2. 三层的职责划分

咱们用一张图来理解 MVVM 的分层结构:

View 层 Activity / Fragment XML 布局 DataBinding 绑定 观察 ViewModel 层 LiveData / StateFlow 业务逻辑 状态管理 调用 Repository 层 数据源统一入口 缓存策略 数据转换 Model 层 数据实体(data class) 网络数据模型 / 数据库实体

你看,数据流向是单向的:View 观察 ViewModel,ViewModel 调用 Repository,Repository 从 Model 层获取数据。反过来,View 不能直接操作 Model,ViewModel 也不能持有 View 的引用。这就是 MVVM 的「规矩」。

3. Repository 模式:数据源的统一管家

Repository 模式说白了就是给数据源加一个中间层。你的数据可能来自网络、本地数据库、或者内存缓存。如果没有 Repository,ViewModel 就得自己判断从哪拿数据——那代码就乱套了。

我曾经接手过一个项目,一个 ViewModel 里同时调了 Retrofit 和 Room,还自己写缓存逻辑。结果呢?网络请求改了,ViewModel 也得改;数据库表结构变了,ViewModel 还得改。这哪是 MVVM,分明是「大泥球」。

正确的做法是这样的:

// 数据源接口
interface UserDataSource {
    suspend fun getUsers(): List<User>
}

// 网络数据源
class UserRemoteDataSource @Inject constructor(
    private val api: UserApi
) : UserDataSource {
    override suspend fun getUsers(): List<User> {
        return api.fetchUsers()
    }
}

// 本地数据源
class UserLocalDataSource @Inject constructor(
    private val dao: UserDao
) : UserDataSource {
    override suspend fun getUsers(): List<User> {
        return dao.getAllUsers()
    }
}

// Repository —— 统一入口
class UserRepository @Inject constructor(
    private val remote: UserRemoteDataSource,
    private val local: UserLocalDataSource
) {
    suspend fun getUsers(): Result<List<User>> {
        return try {
            // 先尝试网络
            val users = remote.getUsers()
            // 缓存到本地
            local.cacheUsers(users)
            Result.success(users)
        } catch (e: Exception) {
            // 网络失败,走本地缓存
            val cached = local.getUsers()
            if (cached.isNotEmpty()) {
                Result.success(cached)
            } else {
                Result.failure(e)
            }
        }
    }
}
我的习惯:Repository 只返回 Result 类型,不直接抛异常。这样 ViewModel 可以统一处理成功/失败状态,UI 层只需要观察一个 LiveData 就够了。

4. ViewModel 的正确写法

ViewModel 的核心是状态管理。我建议你用 StateFlow 替代 LiveData,因为 StateFlow 天然支持协程,而且不会出现 LiveData 的「粘性事件」问题。

class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {

    // 使用 StateFlow 管理 UI 状态
    private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()

    init {
        loadUsers()
    }

    fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            repository.getUsers().fold(
                onSuccess = { users ->
                    _uiState.value = UiState.Success(users)
                },
                onFailure = { error ->
                    _uiState.value = UiState.Error(error.message ?: "未知错误")
                }
            )
        }
    }
}

// 密封类定义 UI 状态
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

你想想看,这样写的好处是什么?UI 层只需要观察 uiState,根据不同的状态展示不同的界面。Loading 就显示进度条,Success 就展示列表,Error 就显示错误提示。逻辑清晰,不容易出错。

5. DataBinding:让 XML 活起来

DataBinding 是 MVVM 的「最后一公里」。它让你在 XML 里直接绑定 ViewModel 的数据,省去了 findViewById 和手动赋值的麻烦。

不过说实话,DataBinding 刚出来的时候,我挺抵触的。总觉得在 XML 里写表达式怪怪的。但用习惯了之后,真香。

来看一个例子:

<!-- activity_user.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.app.UserViewModel" />
    </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="@{viewModel.userName}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.loadUsers()}"
            android:text="加载用户" />

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" />

    </LinearLayout>
</layout>
注意:DataBinding 的表达式里不要写复杂逻辑。我曾经见过有人在 XML 里写三目运算符嵌套——那代码简直没法维护。复杂逻辑请放在 ViewModel 里,XML 只做简单的数据展示和事件绑定。

6. 避坑指南:我踩过的那些坑

做 MVVM 架构,有几个坑是绕不开的。我一个个说:

  • ViewModel 里不要持有 View 的引用——这会导致内存泄漏。我刚开始学 MVVM 时犯过这个错,结果 Activity 销毁了,ViewModel 还在持有它的引用,GC 都回收不了。
  • DataBinding 的 ViewStub 要小心——ViewStub 在 inflate 之前,绑定是不生效的。我曾经调试了半天,发现数据没显示,结果是因为 ViewStub 还没 inflate。
  • StateFlow 的 collect 要在 lifecycle 范围内——用 repeatOnLifecycle 或者 flowWithLifecycle,否则会一直收集,即使界面不可见。
  • Repository 不要返回 null——用 Result 或者 sealed class 包装,让调用方明确知道成功还是失败。

7. 实战:一个完整的 MVVM 流程

咱们把上面的知识点串起来,写一个完整的用户列表功能:

// 1. Model —— 数据实体
data class User(
    val id: Long,
    val name: String,
    val avatar: String
)

// 2. Repository —— 数据源统一入口
class UserRepository(private val api: UserApi, private val dao: UserDao) {
    suspend fun getUsers(): Result<List<User>> { /* 略 */ }
}

// 3. ViewModel —— 业务逻辑和状态管理
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            repository.getUsers().onSuccess { _users.value = it }
        }
    }
}

// 4. View —— Activity 或 Fragment
class UserActivity : AppCompatActivity() {
    private lateinit var binding: ActivityUserBinding
    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_user)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this

        viewModel.loadUsers()
    }
}

你看,整个流程非常清晰。View 只负责绑定和展示,ViewModel 只负责逻辑和状态,Repository 只负责数据获取。各司其职,互不干扰。

8. 总结

MVVM 架构不是银弹,但它确实能帮你写出更清晰、更可测试的代码。我个人觉得,学习 MVVM 的关键不在于记住那些概念,而在于理解数据流向职责划分

记住一句话:View 是傻的,ViewModel 是聪明的,Repository 是万能的。View 只负责展示,ViewModel 负责决策,Repository 负责提供数据。这样,你的项目才能经得起时间的考验。

最后分享一个我自己的经验:刚开始用 MVVM 时,不要追求一步到位。先从 Repository 模式开始,把数据源统一起来;然后引入 ViewModel,把业务逻辑从 Activity 里抽出来;最后再用 DataBinding 简化 UI 代码。一步一步来,稳扎稳打。

公众号:蓝海资料掘金营,微信deep3321