10、MVVM架构实践:ViewModel与Repository模式、数据流从Repository到ViewModel再到UI的完整链路

好,咱们今天聊点实在的。前面几章我们把Retrofit、Coroutine、Flow一个个拆开讲了,现在该把它们串起来了。说白了,就是搭建一个完整的网络层数据链路——从服务器拿到数据,一路送到UI界面显示出来。

这个链路的核心,就是MVVM架构里的ViewModel和Repository。我这些年带团队做项目,发现很多新人容易在这块栽跟头。要么把网络请求直接写在Activity里,要么ViewModel里塞满了业务逻辑。嗯,今天咱们就把这条链路彻底捋清楚。

10.1 为什么需要Repository?

先问个问题:你的数据从哪里来?

可能是网络,可能是本地数据库,也可能是内存缓存。如果ViewModel直接调用这些数据源,一旦数据源变了,ViewModel也得跟着改。这耦合度,你想想看,多难受。

Repository就是中间人。它把数据源藏在自己身后,ViewModel只需要跟Repository打交道。至于数据是从网络拿还是从缓存读,ViewModel根本不用关心。

核心原则:ViewModel只依赖Repository,不依赖具体数据源。这样切换数据源时,只需要改Repository内部实现,ViewModel纹丝不动。

我在项目中遇到过这样的场景:早期版本数据全部走网络,后来加了本地缓存。如果当时没有Repository层,那改动量可就大了去了。有了Repository,只需要在Repository内部加个缓存逻辑,上层代码一行都不用改。

10.2 ViewModel的正确打开方式

ViewModel的生命周期比Activity/Fragment长,它会在配置变更时存活下来。这个特性决定了它适合做两件事:

  • 持有UI状态
  • 协调数据流

但注意,ViewModel不应该持有任何对Activity/Fragment的引用。我见过有人把Context传进ViewModel,这其实是个坑。一旦Activity重建,旧的ViewModel还在,但持有的Context已经失效了。

避坑指南:我曾经在ViewModel里直接传了Activity的Context,结果屏幕旋转后,ViewModel还在用旧的Context更新UI,直接崩溃。后来改用ApplicationContext或者干脆用SavedStateHandle,再也没出过这问题。

10.3 完整链路:从Repository到UI

好,咱们直接上代码。先看Repository层:

// Repository层
class UserRepository(
    private val api: UserApi,
    private val dao: UserDao
) {
    fun getUsers(): Flow<Result<List<User>>> = flow {
        // 先发缓存
        val cached = dao.getAllUsers()
        if (cached.isNotEmpty()) {
            emit(Result.success(cached))
        }
        
        // 再请求网络
        try {
            val response = api.fetchUsers()
            dao.insertAll(response.data)
            emit(Result.success(response.data))
        } catch (e: Exception) {
            // 网络失败,如果缓存有数据就不报错
            if (cached.isEmpty()) {
                emit(Result.failure(e))
            }
        }
    }
}

这里用了Flow,每次emit都会把数据推给订阅者。注意我用了Result包装,这样ViewModel可以统一处理成功和失败的情况。

再看ViewModel层:

// ViewModel层
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    
    private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()
    
    init {
        loadUsers()
    }
    
    private fun loadUsers() {
        viewModelScope.launch {
            repository.getUsers().collect { result ->
                _uiState.value = when (result) {
                    is Result.Success -> UiState.Success(result.data)
                    is Result.Failure -> UiState.Error(result.exception.message ?: "未知错误")
                }
            }
        }
    }
}

这里有个细节:我用viewModelScope启动协程,当ViewModel被清除时,协程会自动取消。你想想看,如果忘了这步,Activity都销毁了,网络请求还在跑,那不是浪费资源吗?

最后是UI层:

// UI层(Fragment中)
class UserFragment : Fragment() {
    
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is UiState.Loading -> showLoading()
                    is UiState.Success -> showUsers(state.data)
                    is UiState.Error -> showError(state.message)
                }
            }
        }
    }
}

这条链路走下来,数据从Repository的Flow出发,经过ViewModel的StateFlow转换,最终到达UI层的collect。每一步都各司其职,清清楚楚。

10.4 数据流示意图

为了让你更直观地理解这条链路,我画了张图:

MVVM数据流完整链路 数据源 网络API / 本地数据库 / 缓存 Repository 数据协调、缓存策略、错误处理 ViewModel 持有UI状态、协调数据流 UI (Activity/Fragment) 观察StateFlow、渲染界面 Flow Flow<Result> StateFlow 数据从数据源出发,经过Repository的转换, 通过ViewModel的StateFlow,最终到达UI层

10.5 状态管理:用Sealed Class定义UI状态

刚才代码里用到了UiState,这是个sealed class。我习惯这样定义:

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

为什么用sealed class?因为它强制你处理所有状态。如果用普通类,你可能会忘了处理Loading或Error状态。sealed class配合when表达式,编译器会检查你是否覆盖了所有分支。

个人习惯:我一般会在UiState里再加一个Empty状态,用来表示数据为空的情况。比如用户列表为空,这时候显示"暂无数据"而不是Loading或Error。

10.6 错误处理:别让异常穿透到UI

错误处理是网络层最容易出问题的地方。我的原则是:异常在Repository层捕获,用Result包装后往上抛

为什么?因为ViewModel和UI层不应该知道网络异常的具体细节。它们只需要知道"成功了"还是"失败了",以及失败时展示什么信息。

我曾经见过一个项目,网络异常直接抛到UI层,结果Fragment里try-catch满天飞。后来重构时,把所有异常处理都收拢到Repository里,代码清爽多了。

10.7 测试:这条链路怎么测?

MVVM架构的一大好处就是可测试性强。咱们可以分层测试:

  • 测试Repository:Mock掉数据源,验证Flow的emit逻辑是否正确
  • 测试ViewModel:Mock掉Repository,验证StateFlow的状态变化是否符合预期
  • 测试UI:用FragmentScenario,验证UI在不同状态下的渲染是否正确

说白了,每一层都可以独立测试,互不干扰。这就是解耦带来的好处。

10.8 总结

这条链路其实不复杂,核心就三句话:

  1. Repository负责跟数据源打交道,返回Flow
  2. ViewModel把Flow转成StateFlow,持有UI状态
  3. UI层观察StateFlow,根据状态渲染界面

记住这个模式,以后不管项目怎么变,网络层这块你都能稳得住。


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