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 数据流示意图
为了让你更直观地理解这条链路,我画了张图:
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 总结
这条链路其实不复杂,核心就三句话:
- Repository负责跟数据源打交道,返回Flow
- ViewModel把Flow转成StateFlow,持有UI状态
- UI层观察StateFlow,根据状态渲染界面
记住这个模式,以后不管项目怎么变,网络层这块你都能稳得住。
公众号:蓝海资料掘金营,微信deep3321