MVC → MVP → MVVM 的本质,是不断把 Activity 从"上帝类"里解救出来。

Model
Model层:不是简单的Entity类,而是代表一整个Entity / DAO / API / Repository的所有集合。
获取数据流程为什么要这么设计?
- 单一职责:一个类只因为一个原因改变,如果不分层,一个类会因为 4 种原因被改变。

- 可维护性 :一个如果不分层的Model层只有一个
Repository文件,改数据库要动Repository,改接口要动Repository,改缓存策略要动Repository,测试极其困难。

- 可测试性:单元测试中这样每层就可以独立测试,不会耦合严重,每次功能测试都要mock一大堆对象。

- 可替代性:高层不依赖底层实现,只依赖接口。

分层表面只是"多写代码",但是让每一层"只为一个理由改变"。
这是为了遵循单一职责原则和关注点分离。Entity 只描述数据结构;DAO 负责本地数据库访问;API 负责网络通信;Repository 作为数据层的总入口,封装数据来源决策、缓存策略和错误处理逻辑。分层带来了更好的可维护性、可测试性和可替换性,使得上层业务不依赖具体的数据实现。
View
中间的V在不同架构中都代表View的概念,负责界面展示角色,狭义指代不同的View对象,界面XML、Compose就是典型View角色。
在MVC中,Activity在管理View同时又有业务逻辑;演化到MVP中,Activity彻底变成了单纯的View角色。
MVC
MVC = Model + View + Controller
Controller(控制器,中间调度层),MVC是Android最初的官方形态,,Activity/Fragment是Controller,管理非常多的View。极容易导致臃肿、出现难以维护的问题,往往几千行的Activity就是这么来的。
kotlin
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
findViewById<Button>(R.id.btnLogin).setOnClickListener {
val user = findViewById<EditText>(R.id.etUser).text.toString()
val pwd = findViewById<EditText>(R.id.etPwd).text.toString()
// 1. 校验
if (user.isEmpty()) {
Toast.makeText(this, "用户名不能为空", SHORT).show()
return
}
// 2. 调 Model
val result = UserRepository.login(user, pwd)=
// 3. 更新 View
if (result) {
startActivity(Intent(this, MainActivity::class.java))
} else {
Toast.makeText(this, "登录失败", SHORT).show()
}
}
}
}

在 Android 早期,官方推荐使用 MVC 架构:XML 作为 View,Activity/Fragment 作为 Controller,Model 负责数据。但由于 Activity 同时承担了 View 和 Controller 的职责,导致代码臃肿、难以测试。随后 Google 推出了 MVP,通过 Presenter 和 View 接口解耦;2017 年后全面转向 MVVM,利用 ViewModel 和 LiveData / StateFlow 实现响应式数据驱动,彻底解决了生命周期和耦合问题。
缺点
Activity同时承担了View和Controller的职责,导致代码臃肿、难以测试。
MVP
MVP = Model + View + Presenter
kotlin
// View
class LoginActivity : AppCompatActivity(), LoginView {
private val presenter = LoginPresenter(this)
fun onClickLogin() {
presenter.login(username, password)
}
override fun showError(msg: String) {
Toast.makeText(this, msg, LENGTH_SHORT).show()
}
}
// Presenter
class LoginPresenter(private val view: LoginView) {
fun login(user: String, pwd: String) {
if (user.isEmpty()) {
view.showError("用户名不能为空")
return
}
// 调用 Model
}
}
MVP中需要做的事:
-
新增一个
View接口,抽象出若干方法,被Activity实现; -
新增一个
Presenter类,Presenter依赖注入View接口对象,即Acticity。 -
Activity中组合一个Presenter对象,所有业务逻辑在Activity中被封装成函数。
用户事件派发下来,这些封装函数直接调用Presenter对象中的View接口方法,接口方法已被实现在Activity中,本质上还是调用Activity,
原生MVC :Activity既是View又是Controller,业务代码全部写在Activity里,代码强依赖Context、控件、生命周期,单元测试必须跑模拟器 / 真机,测试成本极高。
MVP :所有业务逻辑全部剥离到Presenter,Presenter只依赖抽象IView接口,不依赖任何Android框架类。 单元测试时,不用创建Activity,直接手写一个MockIView假实现注入Presenter,就能完整测试登录、列表加载等业务逻辑。
缺点
View 和 Model 通过 Presenter 手动对接,接口满天飞,解耦但臃肿,Presenter持有View引用,如果Activity销毁后没处理好,极易导致内存泄漏。
题外:MVP双向循环强引用
调用链:
Activity------>Presenter------>View接口------>Activity
MVP存在双向循环强引用 ,但它不是无法解决的死循环;通过页面销毁时 detachView() 将 Presenter 中的 View 引用置空,手动切断反向引用链,GC 就能正常回收两个对象,彻底解决内存泄漏问题。
MVVM
MVP 通过Presenter持有View接口来实现解耦,View和Presenter相互引用,通信依赖接口回调;
MVVM 通过数据绑定/响应式流(StateFlow/LiveData)实现View和ViewModel的自动同步,View只观察数据,不感知ViewModel的存在。
MVVM解决了MVP接口膨胀和生命周期耦合的问题,是目前 Android 官方推荐的架构。
LiveData 实战
LiveData基于数据订阅模式的界面刷新方案,配合ViewModel使用,是Android Jetpack专属组件。将传统项目改造成MVVM架构就需要:
- 定义ViewModel :引入
JetPack组件库的ViewModel,然后新建一个ViewModel实现类,该类要定义好行为函数、LiveData对象(常见为MutableLiveData类型)。行为函数内部封装业务逻辑,基本上是对LiveData对象setValue/postValue操作,每一个LiveData对应每一个需要刷新的控件信息单元。 - 创建ViewModel :
Activity/Fragment中创建一个该ViewModel实例,按键逻辑绑定ViewModel中的行为函数。 - 观察LiveData :定义注册订阅函数,使用
ViewModel对象获取暴露的LiveData的值,调用observe函数进行观察,回调里执行相应的刷新逻辑。在Activity/Fragment中onCreate调用注册订阅函数。
View层:Activity
kotlin
class EntranceActivity : AppCompatActivity() {
// ViewBinding 绑定布局
private lateinit var binding: ActivityEntranceBinding
// 获取ViewModel(页面重建自动复用,数据不丢失)
private val loginVm: LoginViewModel by lazy {
ViewModelProvider(this)[LoginViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEntranceBinding.inflate(layoutInflater)
setContentView(binding.root)
// 1. 监听ViewModel的LiveData数据变化,自动刷新UI
observeViewModel()
// 2. 按钮点击,仅转发事件给ViewModel,无任何业务判断
binding.btnLogin.setOnClickListener {
val name = binding.etUsername.text.toString()
val pwd = binding.etPwd.text.toString()
loginVm.login(name, pwd)
}
}
// 订阅ViewModel的数据,数据变化自动回调
private fun observeViewModel() {
// 加载状态控制进度条显示/隐藏
loginVm.loading.observe(this) { isLoading ->
binding.progress.visibility = if (isLoading) android.view.View.VISIBLE else android.view.View.GONE
binding.btnLogin.isEnabled = !isLoading
}
// 吐司提示
loginVm.toastMsg.observe(this) { msg ->
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
// 登录成功跳转
loginVm.loginSuccess.observe(this) { user ->
user ?: return@observe
Toast.makeText(this, "登录成功:${user.username}", Toast.LENGTH_LONG).show()
// 跳转主页逻辑...
}
}
}
Model层:Repository
kotlin
// UserRepository.kt Model仓库
class UserRepository {
// 模拟登录网络请求
suspend fun login(username: String, pwd: String): Result<User> {
// 模拟网络延迟
kotlinx.coroutines.delay(1000)
return if (username == "admin" && pwd == "123456") {
Result.success(User("admin", "abc123xyz"))
} else {
Result.failure(Exception("账号或密码错误"))
}
}
}
// User.kt Model实体
data class User(
val username: String,
val token: String
)
ViewModel层:ViewModel
kotlin
class LoginViewModel : ViewModel() {
// 私有可变LiveData(内部修改)
private val _loading = MutableLiveData<Boolean>()
private val _toastMsg = MutableLiveData<String>()
private val _loginSuccess = MutableLiveData<User?>()
// 对外暴露只读LiveData(View只能观察,不能修改)
val loading: LiveData<Boolean> = _loading
val toastMsg: LiveData<String> = _toastMsg
val loginSuccess: LiveData<User?> = _loginSuccess
// 持有Model仓库
private val userRepo = UserRepository()
// View层触发登录,业务逻辑写这里
fun login(username: String, pwd: String) {
// 输入简单校验
if (username.isBlank() || pwd.isBlank()) {
_toastMsg.postValue("账号密码不能为空")
return
}
// 开启加载状态
_loading.postValue(true)
// viewModelScope:页面销毁自动取消协程,不会内存泄漏
viewModelScope.launch {
val result = userRepo.login(username, pwd)
_loading.postValue(false)
result.onSuccess { user ->
// 登录成功,通知View跳转页面
_loginSuccess.postValue(user)
}.onFailure { err ->
// 登录失败,弹出提示
_toastMsg.postValue(err.message)
}
}
}
// 页面销毁时ViewModel自动销毁,无需手动解绑View
}
缺点
LiveData在历史演进逐渐被淘汰,全方面不如使用kotlin Flow来处理数据绑定/响应式流,ViewModel依然可以完美搭配kotlin Flow的StateFlow,无论选择LiveData还是StateFlow都是MVVM架构思想的体现。
| 特性 | LiveData | StateFlow |
|---|---|---|
| 平台依赖 | 仅 Android 专属 | 纯 Kotlin,跨平台 KMM 可用 |
| 初始值 | 可选,可空 | 强制必须提供初始值 |
| 线程更新 | 主线程 setValue,子线程 postValue 异步 | 任意线程同步.value 赋值 |
| 数据流操作符 | 极少,功能弱 | 全套 Flow 操作符,合并 / 防抖 / 过滤 |
| 生命周期安全收集 | 无原生后台暂停 | repeatOnLifecycle 自动启停 |
| Compose 适配 | 兼容但非首选 | 官方标准方案 |
| 单元测试 | 需要安卓依赖,成本高 | 纯 JVM 直接测试 |
| 去重刷新 | 需手动判断 | 自带 distinctUntilChanged |
StateFlow 实战
ViewModel层:ViewModel
kotlin
// 页面全部UI状态
data class LoginUiState(
val isLoading: Boolean = false,
val toastMessage: String? = null,
val loginUser: User? = null
)
class LoginViewModel : ViewModel() {
private val repo = UserRepository()
// 内部可变StateFlow,只能ViewModel内部修改
private val _uiState = MutableStateFlow(LoginUiState())
// 对外暴露只读StateFlow,View只能观察,不能修改
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun login(username: String, pwd: String) {
if (username.isBlank() || pwd.isBlank()) {
// 更新状态:弹窗提示
updateState { copy(toastMessage = "账号密码不能为空") }
return
}
// 开启加载
updateState { copy(isLoading = true) }
viewModelScope.launch {
val result = repo.login(username, pwd)
result.onSuccess { user ->
// 登录成功,更新用户数据,关闭加载
updateState {
copy(
isLoading = false,
loginUser = user
)
}
}.onFailure { error ->
// 登录失败,弹窗提示,关闭加载
updateState {
copy(
isLoading = false,
toastMessage = error.message
)
}
}
}
}
// 清空toast,防止重复弹窗(单次消费事件处理)
fun clearToast() {
updateState { copy(toastMessage = null) }
}
// 统一更新状态工具方法,简化代码
private fun updateState(block: LoginUiState.() -> LoginUiState) {
_uiState.value = block(_uiState.value)
}
}
这里将LiveData换成MutableStateFlow,前者对私有对象另写getter方法对外暴露,而后者则需要asStateFlow转成StateFlow对象,再对外暴露。
其他地方,将页面信息封装成一个状态类LoginUiState,还将更新状态的工具方法封装成扩展函数updateState,只需要在更新时直接传入copy函数克隆对象即可。不用每次都手动_uiState.value=_uiState.value.copy(isLoading = false)。
View层:Activity
kotlin
class EntranceActivity : AppCompatActivity() {
private lateinit var binding: ActivityEntranceBinding
private val vm: LoginViewModel by lazy {
ViewModelProvider(this)[LoginViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEntranceBinding.inflate(layoutInflater)
setContentView(binding.root)
// 订阅StateFlow状态
observeUiState()
// 按钮点击仅转发事件给ViewModel,无业务逻辑
binding.btnLogin.setOnClickListener {
val name = binding.etUsername.text.toString()
val pwd = binding.etPwd.text.toString()
vm.login(name, pwd)
}
}
private fun observeUiState() {
// lifecycleScope:跟随Activity生命周期
lifecycleScope.launch {
// repeatOnLifecycle:仅页面处于 STARTED 状态(前台可见)时接收数据流
repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.STARTED) {
vm.uiState.collect { state ->
// 每次状态更新都会回调这里,统一刷新UI
// 1. 加载状态
binding.progress.visibility = if (state.isLoading) android.view.View.VISIBLE else android.view.View.GONE
binding.btnLogin.isEnabled = !state.isLoading
// 2. Toast弹窗(单次事件,展示后清空)
state.toastMessage?.let { msg ->
Toast.makeText(this@EntranceActivity, msg, Toast.LENGTH_SHORT).show()
vm.clearToast()
}
// 3. 登录成功
state.loginUser?.let { user ->
Toast.makeText(this@EntranceActivity, "登录成功:${user.username}", Toast.LENGTH_LONG).show()
// 跳转主页逻辑
}
}
}
}
}
}
这里面其他和LiveData使用差别不大,都有订阅函数,重点区别也在这里,lifecycleScope发起了一个协程,repeatOnLifecycle函数同步Activity生命周期,调用StateFlow的collect方法满足条件就会触发数据流回调,collect回调时会返回一个state参数,正是ViewModel里MutableStateFlow(LoginUiState())包装起来的LoginUiState,获取这个state的参数做数据更新。
重头戏#1 lifecycleScope
与viewModelScope类似,lifecycleScope提供的作用域创建的协程会自动跟随页面生命周期销毁,自动取消协程,杜绝内存泄漏,默认调度器是主线程 Dispatchers.Main.immediate,适合 UI 操作。
| 作用域 | 归属 | 取消时机 | 适用场景 |
|---|---|---|---|
| lifecycleScope | Activity/Fragment(页面) | 页面 onDestroy |
监听 UI 数据流、页面生命周期相关任务 |
| viewModelScope | ViewModel | ViewModel 销毁(页面彻底回收、系统杀进程) | 网络请求、业务计算、数据仓库调用 |
kotlin
// Fragment / Activity 内部
lifecycleScope.launch {
delay(3000)
Toast.makeText(this@MainActivity, "弹窗", Toast.LENGTH_SHORT).show()
}
哪怕3秒没执行完,协程会在Activity onDestroy之后直接被取消。
重头戏#2 repeatOnLifecycle
那么只是想后台不触发更新节省资源呢?这个时候就要依赖和生命周期绑定了。 它专门搭配Flow收集,根据页面生命周期自动暂停 / 恢复数据流。

lifecycleScope:页面级协程容器,保证页面销毁自动取消所有任务;repeatOnLifecycle:是lifecycleScope内部的生命周期控制块,专门限制Flow收集仅在页面可见时运行;
接收者Lambda
这里updateState有参数block有点特别,作为Lambda函数它没有参数,但却block(_uiState.value)直接传入了参数,看起来很怪?其实_uiState.value = block(_uiState.value)只是语法糖,实际上等同于_uiState.value = _uiState.value.block(),这样一看block对上了没有参数,但是block怎么又变成了value的函数?这不是乱套了吗?
因为block: LoginUiState.() -> LoginUiState时block被声明成了一个带有隐式接收者的函数类型LoginUiState.(),标记这是一个扩展式Lambda,可以解锁特殊用法,与普通kotlin函数语法有所区别,_uiState.value.block()现在不是value自己的函数,而是代表value会作为隐式接收者对象,变成block的函数体上下文对象,就可以省略it,十分神奇。
kotlin
接收者Lambda:
block: LoginUiState.() -> LoginUiState
调用:updateState { copy(isLoading = true) } //lambda内部this=旧state,省略it,直接copy()
普通Lambda:
block: (LoginUiState) -> LoginUiState
调用:updateState { it.copy(isLoading = true) } //lambda内用it代表传入的旧state,不能直接省略it。
无论普通Lambda还是接收者Lambda,他们封装起来有很多行、使用时又有很多行,看着比只有一行的原始用法复杂太多了:
kotlin
// 原始调用
_uiState.value=_uiState.value.copy(isLoading = false, loginUser = user)
// 定义普通Lambda
private fun updateState1(block: (LoginUiState) -> LoginUiState) {
_uiState.value = block(_uiState.value)
}
// 使用普通Lambda
updateState1 {
it.copy(
isLoading = false,
loginUser = user
)
}
// 定义接收者Lambda
private fun updateState2(block: LoginUiState.() -> LoginUiState) {
_uiState.value = block(_uiState.value)
}
// 使用接收者Lambda
updateState2 {
copy(
isLoading = false,
loginUser = user
)
}
为什么要这么做呢?处处使用原始用法不就好了?其实这里面也有很多讲究。
一、先解决第一个问题:updateState1 vs updateState2,区别只有 it?
updateState1:普通 lambda (LoginUiState) -> LoginUiState
kotlin
updateState1 { it.copy(isLoading = false, loginUser = user) }
updateState2:带接收者 lambda LoginUiState.() -> LoginUiState
kotlin
updateState2 { copy(isLoading = false, loginUser = user) }
肉眼看确实只是少了 it.,语法更干净。这只是表层语法优势,不是核心价值,真正封装的意义不在少打两个字母。
二、第二个关键疑问:原生明明一行搞定,为什么要封装函数?
原生写法:
kotlin
_uiState.value = _uiState.value.copy(isLoading = false, loginUser = user)
看起来很短,但是存在四大硬伤,页面逻辑一多就会爆炸:
不封装缺陷 1:重复样板代码无法统一管控
每次修改状态,必须重复书写 _uiState.value = _uiState.value.xxx假设页面有 20 处修改 UI 状态:
- 原生写法:20 行全部重复这段模板;
- 封装后:20 处只调用
updateState {}。
一旦未来要给所有状态更新加统一逻辑,原生写法会崩溃:举个需求:每次更新状态自动打印新旧状态日志,用于线上 bug 排查。
- 原生写法:20 处全部手动加打印,漏一处就丢日志,改起来巨麻烦;
- 封装写法:只改
updateState这一个方法,全局所有状态更新自动生效:
kotlin
private fun updateState(block: LoginUiState.() -> LoginUiState) {
val old = _uiState.value
val new = block(old)
// 统一埋点日志,所有状态变更自动打印
println("旧状态:$old 新状态:$new")
_uiState.value = new
}
这是封装最核心的价值:统一收口、统一拦截、统一附加逻辑。
不封装缺陷 2:状态更新逻辑可扩展
后续可以轻松增加通用能力:
- 自动去重:新旧状态完全一致时不更新 StateFlow,减少无用 UI 刷新
- 全局状态拦截:比如登录失效统一跳转登录页
- 埋点、日志、调试工具注入这些通用逻辑只需要写一次,全部状态修改自动生效,原生写法做不到。
不封装缺陷 3:统一代码规范,多人协作统一
项目规定:禁止直接操作 _uiState.value,所有状态修改必须通过 updateState 代码评审一眼就能识别哪里在修改 UI 状态,全局检索 updateState 就能找到所有 UI 变更点;原生散写 _uiState.value 很难全局检索梳理页面数据流。
封装总结
对比三种写法长短,分场景看
单次简单修改(你现在的单行场景),适合原生最短,看起来最清爽封装:多了函数调用大括号,代码行数变多。
大型复杂页面(真实项目常态),页面 30 + 处修改状态、需要统一日志、并发安全、全局拦截:原生写法会产生大量重复垃圾代码,维护成本指数上升;封装写法收益巨大,是工程化取舍:牺牲单行简洁,换取全局可维护性。
带接收者 lambda(updateState2)额外小优势
- 代码视觉干净,没有到处飘
it.; - 如果 UiState 里有扩展函数,内部可以直接调用,不需要
it.:
kotlin
// 给UiState写扩展函数
fun LoginUiState.clearInput(): LoginUiState {
return this.copy(username = "", password = "")
}
// 使用带接收者写法,直接调用扩展方法
updateState { clearInput() }
// 普通lambda必须加it
updateState1 { it.clearInput() }
-
updateState1和updateState2的表层区别确实只是少写it.,属于语法糖优化,不是封装的核心目的; -
封装
updateState根本不是为了 "少写代码",而是为了:- 统一收口所有状态更新,批量增加通用逻辑(日志、拦截、去重)
- 解决原生两次读取 value 带来的并发 bug
- 统一团队编码规范,方便代码检索与维护
-
单行简单逻辑原生代码看着舒服,但商业项目复杂页面中,原生写法维护成本极高,工程开发会优先选择封装收口方案,是长期可维护性的取舍。
简单一句话:Demo 简单页面原生写法够用,线上长期维护的商业项目必须封装收口。