📱 系列二:MVVM 深度实战与项目重构| 第4篇
MVVM 完整架构搭建:从零打造企业级框架(Base 封装、全局状态与生命周期铁律)
本文导读
前三篇我们解决了"为什么要用架构"和"代码怎么拆"的问题。
现在,我们要进入最硬核的落地环节 :手把手搭建一个企业级的 MVVM 框架 。
很多团队虽然用了 MVVM,却依然混乱:有的在 ViewModel 里弹 Toast,有的在 Activity 里写业务逻辑,有的因为屏幕旋转导致数据丢失。
本文将通过 标准化的分层职责、严谨的 Base 封装、全局状态管理、以及生命周期规范 ,为你提供一套拿来即用 的 MVVM 脚手架。
建议结合 IDE 食用,文末附有完整项目结构图。
0. 痛点复盘:为什么你的 MVVM 像个"半成品"?
先来看几个常见的 MVVM 烂代码场景:
场景一:ViewModel 里直接弹 Toast(破坏分层)
kotlin
// ❌ 错误示例
class LoginViewModel : ViewModel() {
fun login() {
// ...
Toast.makeText(getApplication(), "登录成功", Toast.LENGTH_SHORT).show() // 灾难!
}
}
场景二:Activity 里直接处理业务逻辑(退化成 MVC)
kotlin
// ❌ 错误示例
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 直接在 Activity 里写校验逻辑
if (etAccount.text.toString().length < 6) {
showError("账号长度不足")
}
}
}
场景三:屏幕旋转后数据丢失(生命周期管理失败)
kotlin
// ❌ 错误示例
class LoginActivity : AppCompatActivity() {
private var isLoading = false // 旋转屏幕后,这个值会重置
}
这些问题的根源只有一个:没有建立 MVVM 的"工程化规范"。
1. 企业级 MVVM 的分层职责(再次钉死)
在进入代码前,我们必须再次明确 三层架构 的边界。这是后续所有封装的基础。
#mermaid-svg-99AQQQa9Hov9tgnZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-99AQQQa9Hov9tgnZ .error-icon{fill:#552222;}#mermaid-svg-99AQQQa9Hov9tgnZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-99AQQQa9Hov9tgnZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-99AQQQa9Hov9tgnZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-99AQQQa9Hov9tgnZ .marker.cross{stroke:#333333;}#mermaid-svg-99AQQQa9Hov9tgnZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-99AQQQa9Hov9tgnZ p{margin:0;}#mermaid-svg-99AQQQa9Hov9tgnZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-99AQQQa9Hov9tgnZ .cluster-label text{fill:#333;}#mermaid-svg-99AQQQa9Hov9tgnZ .cluster-label span{color:#333;}#mermaid-svg-99AQQQa9Hov9tgnZ .cluster-label span p{background-color:transparent;}#mermaid-svg-99AQQQa9Hov9tgnZ .label text,#mermaid-svg-99AQQQa9Hov9tgnZ span{fill:#333;color:#333;}#mermaid-svg-99AQQQa9Hov9tgnZ .node rect,#mermaid-svg-99AQQQa9Hov9tgnZ .node circle,#mermaid-svg-99AQQQa9Hov9tgnZ .node ellipse,#mermaid-svg-99AQQQa9Hov9tgnZ .node polygon,#mermaid-svg-99AQQQa9Hov9tgnZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-99AQQQa9Hov9tgnZ .rough-node .label text,#mermaid-svg-99AQQQa9Hov9tgnZ .node .label text,#mermaid-svg-99AQQQa9Hov9tgnZ .image-shape .label,#mermaid-svg-99AQQQa9Hov9tgnZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-99AQQQa9Hov9tgnZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-99AQQQa9Hov9tgnZ .rough-node .label,#mermaid-svg-99AQQQa9Hov9tgnZ .node .label,#mermaid-svg-99AQQQa9Hov9tgnZ .image-shape .label,#mermaid-svg-99AQQQa9Hov9tgnZ .icon-shape .label{text-align:center;}#mermaid-svg-99AQQQa9Hov9tgnZ .node.clickable{cursor:pointer;}#mermaid-svg-99AQQQa9Hov9tgnZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-99AQQQa9Hov9tgnZ .arrowheadPath{fill:#333333;}#mermaid-svg-99AQQQa9Hov9tgnZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-99AQQQa9Hov9tgnZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-99AQQQa9Hov9tgnZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-99AQQQa9Hov9tgnZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-99AQQQa9Hov9tgnZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-99AQQQa9Hov9tgnZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-99AQQQa9Hov9tgnZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-99AQQQa9Hov9tgnZ .cluster text{fill:#333;}#mermaid-svg-99AQQQa9Hov9tgnZ .cluster span{color:#333;}#mermaid-svg-99AQQQa9Hov9tgnZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-99AQQQa9Hov9tgnZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-99AQQQa9Hov9tgnZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-99AQQQa9Hov9tgnZ .icon-shape,#mermaid-svg-99AQQQa9Hov9tgnZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-99AQQQa9Hov9tgnZ .icon-shape p,#mermaid-svg-99AQQQa9Hov9tgnZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-99AQQQa9Hov9tgnZ .icon-shape rect,#mermaid-svg-99AQQQa9Hov9tgnZ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-99AQQQa9Hov9tgnZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-99AQQQa9Hov9tgnZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-99AQQQa9Hov9tgnZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户事件
数据变化
调用
数据层 (Model)
Repository (仓库)
Remote (网络)
Local (数据库/SP)
ViewModel 层 (Business Logic)
ViewModel
LiveData / StateFlow
UseCase (业务用例)
UI 层 (View)
Activity / Fragment
ViewBinding / Compose
| 层级 | 必须做的 | 绝对禁止做的 |
|---|---|---|
| UI 层 | 1. 渲染 UI 2. 收集用户事件 3. 观察 ViewModel 状态 | 1. 写业务逻辑 2. 直接操作数据库/网络 3. 持有 ViewModel 以外的 Context |
| ViewModel | 1. 持有 UI State 2. 处理业务逻辑 3. 调用 UseCase/Repository | 1. 持有 View/Context 2. 直接操作 UI(Toast/Dialog) 3. 写 Android 平台特定代码 |
| 数据层 | 1. 提供数据 2. 缓存策略 3. 数据转换 | 1. 依赖 ViewModel 2. 处理 UI 逻辑 |
2. 搭建骨架:Base 类的标准化封装
为了减少样板代码,并确保规范被强制执行,我们需要一套 Base 类。
2.1 BaseViewModel:生命周期与异常处理
目标:
- 自动管理协程作用域(页面销毁自动取消)。
- 统一异常处理。
- 提供通用的 Loading/Error 状态。
代码实现:
kotlin
// ✅ 企业级 BaseViewModel
abstract class BaseViewModel : ViewModel() {
// 1. 全局通用状态(分散 State,正统 MVVM)
private val _loading = MutableLiveData(false)
val loading: LiveData<Boolean> = _loading
private val _error = SingleLiveEvent<String>()
val error: LiveData<String> = _error
// 2. 安全执行协程(自动处理 Loading 和 Error)
protected fun launch(
showLoading: Boolean = true,
block: suspend () -> Unit
) {
viewModelScope.launch {
try {
if (showLoading) _loading.value = true
block()
} catch (e: Exception) {
_error.value = e.message ?: "未知错误"
} finally {
if (showLoading) _loading.value = false
}
}
}
// 3. 不带 Loading 的执行(用于不需要 Loading 的场景)
protected fun launchSilent(block: suspend () -> Unit) {
viewModelScope.launch {
try {
block()
} catch (e: Exception) {
_error.value = e.message ?: "未知错误"
}
}
}
}
2.2 BaseActivity:ViewBinding 与 状态收集
目标:
- 自动初始化 ViewBinding。
- 自动收集 BaseViewModel 的全局状态(Loading/Error)。
- 统一处理 Back 键、权限等。
代码实现:
kotlin
// ✅ 企业级 BaseActivity
abstract class BaseActivity<VB : ViewBinding, VM : BaseViewModel> : AppCompatActivity() {
protected lateinit var binding: VB
protected abstract val viewModel: VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 初始化 ViewBinding(反射方式,也可以写成抽象方法让子类实现)
binding = getViewBinding()
setContentView(binding.root)
// 2. 收集全局状态
setupObservers()
// 3. 初始化 UI
initView()
// 4. 加载数据
loadData()
}
private fun setupObservers() {
// 监听 Loading
viewModel.loading.observe(this) { isLoading ->
if (isLoading) showLoading() else hideLoading()
}
// 监听 Error
viewModel.error.observe(this) { errorMsg ->
showError(errorMsg)
}
}
// 抽象方法:子类必须实现
protected abstract fun getViewBinding(): VB
protected abstract fun initView()
protected abstract fun loadData()
// 通用 UI 方法
protected open fun showLoading() {
// 可以用一个全局的 ProgressDialog
}
protected open fun hideLoading() {}
protected open fun showError(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
2.3 实战:用 Base 类重构 Login 页面
LoginViewModel:
kotlin
class LoginViewModel @HiltViewModel constructor(
private val loginUseCase: LoginUseCase
) : BaseViewModel() {
// 页面特有的 State
private val _loginSuccess = SingleLiveEvent<Boolean>()
val loginSuccess: LiveData<Boolean> = _loginSuccess
fun login(account: String, password: String) {
// 使用 BaseViewModel 提供的 launch 方法
launch {
val result = loginUseCase(account, password)
_loginSuccess.value = result.isSuccess
}
}
}
LoginActivity:
kotlin
@AndroidEntryPoint
class LoginActivity : BaseActivity<ActivityLoginBinding, LoginViewModel>() {
override val viewModel: LoginViewModel by viewModels()
override fun getViewBinding(): ActivityLoginBinding {
return ActivityLoginBinding.inflate(layoutInflater)
}
override fun initView() {
binding.btnLogin.setOnClickListener {
viewModel.login(
binding.etAccount.text.toString(),
binding.etPwd.text.toString()
)
}
}
override fun loadData() {
// 可以在这里加载初始数据
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 监听页面特有的 State
viewModel.loginSuccess.observe(this) { success ->
if (success) {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
}
}
}
效果:
- Activity 里 没有任何 try-catch。
- ViewModel 里 没有任何 Toast。
- Loading 和 Error 自动处理。
3. 全局状态统一管理(企业级规范)
在实际项目中,我们会有很多通用的 UI 状态,比如:
- 空数据页面
- 网络异常页面
- 权限申请弹窗
- 重新登录提示
这些状态不应该在每个 ViewModel 里重复定义。
3.1 定义全局 UI 状态
kotlin
// 全局 UI 状态密封类
sealed class GlobalUiState {
object Idle : GlobalUiState()
object Loading : GlobalUiState()
data class Error(val message: String) : GlobalUiState()
data class Empty(val text: String = "暂无数据") : GlobalUiState()
data class Permission(val permission: String) : GlobalUiState()
object ReLogin : GlobalUiState()
}
3.2 全局 ViewModel(单例)
kotlin
@HiltViewModel
class GlobalViewModel @Inject constructor() : BaseViewModel() {
private val _uiState = MutableStateFlow<GlobalUiState>(GlobalUiState.Idle)
val uiState: StateFlow<GlobalUiState> = _uiState.asStateFlow()
fun showLoading() {
_uiState.value = GlobalUiState.Loading
}
fun showEmpty() {
_uiState.value = GlobalUiState.Empty()
}
fun showReLogin() {
_uiState.value = GlobalUiState.ReLogin
}
}
3.3 在 BaseActivity 中统一处理
kotlin
// 在 BaseActivity 中增加
private val globalViewModel: GlobalViewModel by viewModels()
private fun setupGlobalObservers() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
globalViewModel.uiState.collect { state ->
when (state) {
is GlobalUiState.Loading -> showGlobalLoading()
is GlobalUiState.Empty -> showEmptyPage(state.text)
is GlobalUiState.ReLogin -> showReLoginDialog()
else -> Unit
}
}
}
}
}
这样,任何 ViewModel 都可以发送全局状态,而不需要关心 UI 怎么渲染。
4. 单向数据流闭环(数据驱动 UI 的铁律)
MVVM 的核心不是 ViewModel,而是 数据驱动 。为了确保这一点,我们必须遵守以下三条铁律:
铁律一:UI 只能读 State,不能改 State
错误:
kotlin
// ❌ Activity 直接改 ViewModel 的 LiveData
viewModel.loading.value = true
正确:
kotlin
// ✅ Activity 调用 ViewModel 的方法
viewModel.login(...)
// ViewModel 内部自己改 State
铁律二:State 只能由 ViewModel 更新
错误:
kotlin
// ❌ 在 XML 里直接改数据(DataBinding 的双刃剑)
android:text="@={viewModel.account}" // 谨慎使用!容易导致逻辑散落在 XML
正确:
kotlin
// ✅ 使用单向绑定 + 事件
android:text="@{viewModel.account}"
android:afterTextChanged="@{() -> viewModel.updateAccount(...)}"
铁律三:事件必须一次性消费(SingleLiveEvent / Channel)
问题 :屏幕旋转后,LiveData 会再次回调,导致重复弹 Toast 或重复跳转。
解决方案 :使用 SingleLiveEvent 或 Channel。
kotlin
// 推荐方案:Channel (StateFlow + Channel)
class LoginViewModel : BaseViewModel() {
// 1. State(连续状态)
private val _uiState = MutableStateFlow(LoginUiState())
val uiState = _uiState.asStateFlow()
// 2. Effect(一次性事件)
private val _effect = Channel<LoginEffect>()
val effect = _effect.receiveAsFlow()
fun login() {
viewModelScope.launch {
// 更新 State
_uiState.update { it.copy(isLoading = true) }
// 登录成功
_effect.send(LoginEffect.NavigateHome)
}
}
}
sealed class LoginEffect {
object NavigateHome : LoginEffect()
}
5. MVVM 初始化、绑定与销毁的完整生命周期
这是最容易出内存泄漏的地方。
5.1 初始化(Init)
- Activity/Fragment :在
onCreate中初始化 ViewBinding、设置监听器。 - ViewModel:在构造函数中通过 Hilt 注入依赖(Repository/UseCase)。
- 数据加载 :在
onStart或onResume中调用 ViewModel 的加载方法(视业务而定)。
5.2 绑定(Binding)
- ViewBinding :强制使用,替代
findViewById。 - DataBinding:仅在简单页面使用,复杂页面建议只用 ViewBinding + 手动更新。
- Flow/Compose :使用
repeatOnLifecycle(Lifecycle.State.STARTED)收集。
5.3 销毁(Destroy)
这是生命线!
kotlin
// ✅ 正确的销毁姿势
class BaseActivity : AppCompatActivity() {
private val compositeDisposable = CompositeDisposable() // RxJava
private val jobs = mutableListOf<Job>() // 协程
override fun onDestroy() {
super.onDestroy()
// 1. 取消 RxJava 订阅
compositeDisposable.clear()
// 2. 取消协程
jobs.forEach { it.cancel() }
// 3. 解绑 ViewBinding(防止内存泄漏)
binding = null // 如果你用了 lateinit var binding
}
}
class BaseViewModel : ViewModel() {
override fun onCleared() {
super.onCleared()
// 这里可以做一些清理工作
// viewModelScope 会自动取消,但如果有其他资源(如 Socket),在这里关闭
}
}
6. 企业级 MVVM 项目结构(最终形态)
com.example.app
├── presentation
│ ├── base
│ │ ├── BaseActivity.kt
│ │ ├── BaseFragment.kt
│ │ └── BaseViewModel.kt
│ ├── login
│ │ ├── LoginActivity.kt
│ │ ├── LoginViewModel.kt
│ │ └── LoginUiState.kt
│ ├── home
│ │ ├── HomeActivity.kt
│ │ └── HomeViewModel.kt
│ └── main
│ └── MainActivity.kt
│
├── domain
│ ├── model
│ │ └── User.kt
│ ├── repository
│ │ └── UserRepository.kt
│ └── usecase
│ └── LoginUseCase.kt
│
├── data
│ ├── repository
│ │ └── UserRepositoryImpl.kt
│ ├── datasource
│ │ ├── remote
│ │ │ └── UserRemoteDataSource.kt
│ │ └── local
│ │ └── UserLocalDataSource.kt
│ └── model
│ ├── dto
│ │ └── UserDto.kt
│ └── entity
│ └── UserEntity.kt
│
└── infrastructure
├── network
│ └── NetworkClient.kt
├── database
│ └── AppDatabase.kt
└── utils
└── GlobalViewModel.kt
7. 总结:MVVM 的"军规"
- ViewModel 不碰 View:不弹 Toast,不跳页面,不拿 Context。
- Activity 不写逻辑:只负责渲染和转发事件。
- 数据驱动 UI :UI 随数据变,而不是手动
setText。 - 单向数据流:事件向上,状态向下。
- 一次性事件用 Channel:防止旋转屏幕导致重复消费。
- 生命周期安全第一 :
viewModelScope+repeatOnLifecycle。
下期预告 :
系列二:MVVM 深度实战与项目重构 | 第5篇:ViewModel 核心原理与实战避坑
(我们将深入 ViewModel 的源码,解析它是如何在屏幕旋转时存活的,以及如何解决 ViewModelScope 的内存陷阱。)
如果你觉得这套框架对你有帮助,请把它分享给你的团队。统一的规范,比完美的代码更重要。