第39章:MVVM架构:Model-View-ViewModel模式、Repository模式、数据绑定(DataBinding)
说实话,MVVM 这个词在 Android 圈子里已经被说烂了。但真正能把 MVVM 用明白、用舒服的,其实不多。我见过不少项目,说是 MVVM,结果 ViewModel 里塞满了网络请求代码,Activity 里直接操作数据库——嗯,那还不如用 MVC 呢。
这一章,咱们就聊聊 MVVM 的核心三件套:Model-View-ViewModel、Repository 模式、以及 DataBinding。我会结合我自己的踩坑经历,帮你把这些概念落地到实际项目中。
1. MVVM 到底解决了什么问题?
先问个问题:你写 Android 的时候,最头疼的是什么?
我个人觉得,是状态管理。Activity 里一堆回调,数据一变就要手动更新 UI,稍不留神就空指针。更别说屏幕旋转了——那简直是噩梦。
MVVM 的核心思路很简单:把 UI 逻辑和业务逻辑彻底分开。ViewModel 不持有 View 的引用,View 只负责展示,ViewModel 只负责处理数据。这样,你的代码就能经得起屏幕旋转的考验。
我在项目中遇到过一种情况:一个同事把网络请求直接写在 Activity 里,结果每次旋转屏幕都要重新请求。换成 MVVM 后,ViewModel 存活在旋转过程中,数据一次请求,终身受用。
2. 三层的职责划分
咱们用一张图来理解 MVVM 的分层结构:
你看,数据流向是单向的: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)
}
}
}
}
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>
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 代码。一步一步来,稳扎稳打。