系列二:MVVM 深度实战与项目重构 | 第4篇 MVVM 完整架构搭建:从零打造企业级框架(Base 封装、全局状态与生命周期铁律)

📱 系列二: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:生命周期与异常处理

目标

  1. 自动管理协程作用域(页面销毁自动取消)。
  2. 统一异常处理。
  3. 提供通用的 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 与 状态收集

目标

  1. 自动初始化 ViewBinding。
  2. 自动收集 BaseViewModel 的全局状态(Loading/Error)。
  3. 统一处理 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 或重复跳转。

解决方案 :使用 SingleLiveEventChannel

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)。
  • 数据加载 :在 onStartonResume 中调用 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 的"军规"

  1. ViewModel 不碰 View:不弹 Toast,不跳页面,不拿 Context。
  2. Activity 不写逻辑:只负责渲染和转发事件。
  3. 数据驱动 UI :UI 随数据变,而不是手动 setText
  4. 单向数据流:事件向上,状态向下。
  5. 一次性事件用 Channel:防止旋转屏幕导致重复消费。
  6. 生命周期安全第一viewModelScope + repeatOnLifecycle

下期预告

系列二:MVVM 深度实战与项目重构 | 第5篇:ViewModel 核心原理与实战避坑

(我们将深入 ViewModel 的源码,解析它是如何在屏幕旋转时存活的,以及如何解决 ViewModelScope 的内存陷阱。)


如果你觉得这套框架对你有帮助,请把它分享给你的团队。统一的规范,比完美的代码更重要。

相关推荐
逻极1 天前
Hermes Agent深度探索:一个会自我沉淀经验的终端智能体
架构·llm·agent·rag·多智能体系统·hermes agent·hermes
数智顾问1 天前
(151页PPT)XX集团信息化整体架构规划及ERP方案建议书(附下载方式)
大数据·架构
caimouse1 天前
Reactos 第1章 概述
c语言·开发语言·架构
namexingyun1 天前
拆解Fable 5三重安全护栏:模型路由、蒸馏防护与生物安全分类器的技术原理 - 微元算力(weytoken)
java·人工智能·python·安全·架构·ai编程
小短腿的代码世界1 天前
行情快照与增量更新引擎:Qt在高频交易数据分发中的核心架构——你的行情推送为什么延迟了500ms?
开发语言·qt·架构
上海云盾第一敬业销售1 天前
高效阻止网站攻击的WAF防护架构解析
web安全·架构·ddos
意图共鸣1 天前
意图共鸣科技《AI记忆链商业化白皮书3.0》假设场景解析:从母亲到消防员,专属AI如何重塑记忆与传承
人工智能·科技·架构
FPGA小徐1 天前
Xilinx zynq-7000系列FPGA移植Linux操作系统详细教程
fpga开发·架构
王二端茶倒水1 天前
智慧小区宽带无线运营:从网络交付到认证、计费与运维闭环
运维·物联网·架构
ai产品老杨1 天前
基于 Docker 与边缘计算的智能安防架构:解耦 GB28181/RTSP 多协议接入与异构芯片部署(附源码交付与 95% 降本实践)
docker·架构·边缘计算