85、MVVM架构实战:Repository模式、ViewModel、LiveData/Flow、数据绑定

MVVM 架构,说白了就是让代码各司其职。我最早做项目时,Activity 里塞满了网络请求、数据解析、UI 更新,一个文件上千行,改个需求得翻半天。后来用了 MVVM,才真正体会到什么叫「清爽」。今天咱们就把它拆开揉碎了讲清楚。

一、MVVM 的核心思想

MVVM 把界面、数据和业务逻辑分成了三层:

  • Model:数据层,包括网络、数据库、本地文件等数据来源。
  • View:视图层,就是 Activity、Fragment、Compose 这些 UI 组件。
  • ViewModel:连接 View 和 Model 的桥梁,负责处理业务逻辑,管理 UI 状态。

你想想看,ViewModel 不持有 View 的引用,所以屏幕旋转时数据不会丢。这一点,我当年在重构一个电商项目时深有体会——以前用 MVP,每次旋转都要重新加载商品列表,用户体验很差。换成 MVVM 后,数据稳稳地待在 ViewModel 里,用户根本感觉不到旋转过。

关键原则:View 只负责展示和交互,ViewModel 只负责数据和逻辑,Model 只负责数据来源。谁也别越界。

View 层 Activity / Fragment XML / Compose ViewModel 层 LiveData / Flow 业务逻辑 Repository 层 数据统一入口 缓存策略 观察 调用 返回数据 通知更新 本地数据库 网络 API SharedPreferences Repository 统一管理 View 不直接访问数据源

二、Repository 模式——数据统一入口

Repository 是数据层的门面。View 和 ViewModel 不需要知道数据来自网络还是本地,它们只管调用 Repository 的方法就行。

我在项目中遇到过一个问题:多个页面都要获取用户信息,有的直接调 API,有的读缓存,结果数据不一致。后来统一用 Repository 管理,所有地方都走同一个入口,问题就解决了。

// UserRepository.kt
class UserRepository(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    fun getUser(id: String): Flow<Result<User>> = flow {
        // 先读缓存
        val cached = userDao.getUser(id)
        if (cached != null) {
            emit(Result.success(cached))
        }
        
        // 再请求网络
        try {
            val response = apiService.fetchUser(id)
            userDao.insertUser(response.data)
            emit(Result.success(response.data))
        } catch (e: Exception) {
            // 网络失败时,如果缓存有数据就不报错
            if (cached == null) {
                emit(Result.failure(e))
            }
        }
    }
}

我的习惯:Repository 里尽量用 Flow 返回数据,这样 ViewModel 可以灵活选择 collect 或 map。LiveData 虽然好用,但 Flow 在协程世界里更自然。

三、ViewModel——状态持有者

ViewModel 的生命周期比 Activity 长,所以适合存放 UI 状态。我见过有人把网络请求直接写在 ViewModel 里,其实不对——ViewModel 应该调用 Repository,而不是直接操作数据源。

// UserViewModel.kt
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    
    private val _userState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val userState: StateFlow<UiState<User>> = _userState.asStateFlow()
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            repository.getUser(id).collect { result ->
                _userState.value = result.fold(
                    onSuccess = { UiState.Success(it) },
                    onFailure = { UiState.Error(it.message ?: "未知错误") }
                )
            }
        }
    }
}

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>()
}

我曾经踩过的坑:在 ViewModel 里直接暴露 MutableStateFlow 给 View,结果 Fragment 里直接修改了状态,导致数据流混乱。记住:对外只暴露不可变的 StateFlow,修改只能通过 ViewModel 的方法。

四、LiveData vs Flow——怎么选?

特性 LiveData Flow
生命周期感知 原生支持 需配合 repeatOnLifecycle
协程支持 一般 原生支持
操作符丰富度 较少 非常丰富(map、flatMap、combine 等)
适合场景 简单 UI 状态 复杂数据流、协程场景

我个人习惯:新项目全部用 Flow + StateFlow。LiveData 虽然简单,但 Flow 的灵活性和协程生态更强大。不过如果你在维护老项目,LiveData 完全够用,没必要强行迁移。

五、数据绑定——View 和 ViewModel 的桥梁

数据绑定有两种方式:传统的 DataBinding 和 Compose 的 State 驱动。这里我重点讲 DataBinding,因为很多项目还在用 XML。

<!-- activity_user.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.UserViewModel" />
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <TextView
            android:text="@{viewModel.userState.data?.name}"
            android:visibility="@{viewModel.userState is Success ? View.VISIBLE : View.GONE}"
            ... />
            
        <ProgressBar
            android:visibility="@{viewModel.userState is Loading ? View.VISIBLE : View.GONE}"
            ... />
    </LinearLayout>
</layout>

在 Activity 里绑定:

val binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
binding.viewModel = viewModel
binding.lifecycleOwner = this  // 让 LiveData 感知生命周期

一个小技巧:在 XML 里用 BindingAdapter 自定义属性,比如加载图片、格式化时间,能让布局更干净。我经常把 Glide 加载封装成一个 BindingAdapter,一行代码搞定图片显示。

六、完整实战流程

咱们用一个用户列表的例子串起来:

  1. 定义数据源:Room 数据库 + Retrofit API
  2. 创建 Repository:提供 getUserList() 方法,返回 Flow<List<User>>
  3. ViewModel:持有 StateFlow<UiState<List<User>>>,调用 Repository 获取数据
  4. View:通过 DataBinding 或 collect 观察状态,自动更新 UI

你看,每一层职责都很清晰。View 不用管数据从哪来,ViewModel 不用管数据怎么展示,Repository 不用管数据怎么用。这就是 MVVM 的魅力。

核心总结:MVVM 不是银弹,但它能让你的代码结构清晰、易于测试、方便维护。我用了这么多年,最大的感受就是——改需求时不再心惊胆战了。


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