在 Android 开发中,架构选择直接影响代码的可维护性与扩展性。本文带你回顾从 MVC 到 MVVM 的演进历程,剖析每种架构的痛点与优势,并提供实战避坑指南。无论你是刚入门还是想重构项目,都能从中找到启发。

为什么你的 Activity 会变成“屎山”?

早期 Android 开发追求“快”——产品要一个登录页,你创建一个 LoginActivity,写下 findViewById,写一个 OnClickListener,在里面直接调用网络请求,把结果存进 SharedPreferences,然后跳转到主页。整个过程行云流水,半小时搞定。

但问题随之而来。几个月后,需求开始堆叠:增加“记住密码”功能、第三方登录(微信、QQ、Apple)、验证码与密码登录切换、风控校验、协议勾选、防抖处理……你开始往 LoginActivity 里不断塞代码。最终,它变成了一个拥有 3000 行代码的“万能类”。此时哪怕改一个文案,都要小心翼翼,生怕触动了某根脆弱的逻辑链条。

这不是你一个人的问题,而是架构缺失的必然结果。就像写 JavaScript 时把所有逻辑塞进一个函数,最终难以维护。好的架构能像 TypeScript 的类型系统一样,提前约束行为,避免混乱。

⚠️ MVC:Android 世界的“默认陷阱”

教科书上的 MVC(Model-View-Controller)是清晰的,但在 Android 里往往变了味。

理论上的 MVC:用户操作 → Controller 处理 → 更新 Model → 通知 View 刷新。但在 Android 中,由于 XML 布局能力太弱,所有事件(Click、TextChange)都必须回到 ActivityFragment 中处理。这就导致了:View 是 XML 布局,Controller 是 Activity/Fragment,Model 负责数据获取与存储。而 Activity 被迫同时承担 View 和 Controller 的双重角色。

代码实况:经典的 MVC 写法如下:

// ❌ 典型的 MVC 写法(也是“屎山”的起点)
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val etAccount = findViewById<EditText>(R.id.etAccount)
  val etPwd = findViewById<EditText>(R.id.etPwd)
    val btnLogin = findViewById<Button>(R.id.btnLogin)
      val tvTip = findViewById<TextView>(R.id.tvTip)
        // 读取本地缓存
        val sp = getSharedPreferences("config", MODE_PRIVATE)
        val lastAccount = sp.getString("last_account", "")
        etAccount.setText(lastAccount)
        btnLogin.setOnClickListener {
        val account = etAccount.text.toString()
        val pwd = etPwd.text.toString()
        // 1. 校验逻辑(业务)
        if (account.isBlank() || pwd.isBlank()) {
        tvTip.text = "请输入账号密码"
        return@setOnClickListener
        }
        // 2. 更新 UI(View)
        btnLogin.isEnabled = false
        tvTip.text = "登录中..."
        // 3. 网络请求(直接裸线程)
        Thread {
        val result = URL("https://api.xxx.com/login?a=$account&p=$pwd").readText()
        // 4. 回到主线程更新 UI
        runOnUiThread {
        btnLogin.isEnabled = true
        tvTip.text = "登录成功"
        // 5. 存储数据
        sp.edit().putString("last_account", account).apply()
        // 6. 跳转
        startActivity(Intent(this, HomeActivity::class.java))
        finish()
        }
        }.start()
        }
        }
        }

为什么 MVC 会腐烂? 看下面的对比:

致命伤后果
生命周期耦合屏幕旋转或后台回收时,线程还在跑,Activity 已销毁,直接导致 NPE(空指针)。
职责混乱一个类里既有 的操作,又有 的调度,还有 的读写。
无法测试逻辑锁死在 Android 组件中,脱离手机根本跑不起来单元测试。
复用性为零登录逻辑写死在 Activity 里,其他地方想复用?只能复制粘贴。

简单说,MVC 在 Android 中缺乏强制分层,导致 Activity 膨胀。类似 Python 中把所有功能写在一个模块里,初期快,后期痛。

MVP:第一次尝试“解耦”

为了解决 MVC 的耦合问题,MVP(Model-View-Presenter)登场了。核心思想是:把业务逻辑从 Activity 里抽出来,放到 Presenter 中。

MVP 的结构:用户事件 → Presenter 处理 → 通过接口回调通知 View 更新。View(Activity)实现接口,Presenter 持有 View 引用,Model 提供数据。

代码实况

  1. 定义 View 接口:
    interface LoginContract {
    interface View {
    fun showLoading()
    fun hideLoading()
    fun showError(msg: String)
    fun navigateToHome()
    fun isActive(): Boolean // 关键:判断页面是否还活着
    }
    interface Presenter {
    fun attach(view: View)
    fun detach()
    fun login(account: String, pwd: String)
    }
    }
  2. 实现 Presenter:
    class LoginPresenter : LoginContract.Presenter {
    private var view: LoginContract.View? = null
    override fun attach(view: LoginContract.View) {
    this.view = view
    }
    override fun detach() {
    this.view = null // 断开引用,防止内存泄漏
    }
    override fun login(account: String, pwd: String) {
    if (account.isBlank() || pwd.isBlank()) {
    if (view?.isActive() == true) {
    view?.showError("账号密码不能为空")
    }
    return
    }
    view?.showLoading()
    // 模拟网络请求(实际项目会用 Coroutine)
    Thread {
    Thread.sleep(1200)
    if (view?.isActive() == true) {
    // 回到主线程
    Handler(Looper.getMainLooper()).post {
    view?.hideLoading()
    view?.navigateToHome()
    }
    }
    }.start()
    }
    }
  3. Activity 作为 View 层:
    class LoginActivity : AppCompatActivity(), LoginContract.View {
    private lateinit var presenter: LoginPresenter
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_login)
    presenter = LoginPresenter()
    presenter.attach(this)
    findViewById<Button>(R.id.btnLogin).setOnClickListener {
      val account = findViewById<EditText>(R.id.etAccount).text.toString()
        val pwd = findViewById<EditText>(R.id.etPwd).text.toString()
          presenter.login(account, pwd)
          }
          }
          override fun onDestroy() {
          super.onDestroy()
          presenter.detach() // 必须断开,否则内存泄漏
          }
          // ---- 实现 View 接口 ----
          override fun showLoading() {
          findViewById<ProgressBar>(R.id.progressBar).isVisible = true
            }
            override fun hideLoading() {
            findViewById<ProgressBar>(R.id.progressBar).isVisible = false
              }
              override fun showError(msg: String) {
              Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
              }
              override fun navigateToHome() {
              startActivity(Intent(this, HomeActivity::class.java))
              finish()
              }
              override fun isActive(): Boolean = !isFinishing && !isDestroyed
              }

MVP 的得与失

  • ✅ 得:代码分层,Activity 不再是一坨浆糊;Presenter 是纯 Java/Kotlin 类,可脱离手机做单元测试。
  • ⚠️ 失:接口爆炸(一个页面要写 View、Presenter、Contract 三个接口);内存泄漏风险(如果忘了调用 detach());双向依赖容易形成复杂调用链。

MVP 的接口爆炸问题,就像在 Go 中为每个函数写多个接口文件,虽然解耦但增加维护成本。不过它确实是迈向成熟架构的重要一步。

[AFFILIATE_SLOT_1]

MVVM:正统写法(数据驱动 UI)

MVP 虽然解耦,但依然需要手动调用接口(view.showLoading())。MVVM(Model-View-ViewModel)的出现彻底改变了这一点。核心思想只有一个:数据驱动 UI。你不再需要告诉 View “显示 Loading”,只需要改变 ViewModel 里的 isLoading 变量,View 会自动更新。

MVVM 的结构:用户事件 → ViewModel 处理数据 → 数据变化自动通知 View 更新。View 只观察,不指挥。

代码实况(正统 MVVM,无 State 聚合):注意,ViewModel 可以有多个独立的数据状态,不需要强行聚合为一个 UiState

  1. ViewModel 持有数据,提供方法:
    class LoginViewModel : ViewModel() {
    // 多个独立的状态(正统 MVVM 允许这样)
    private val _isLoading = MutableLiveData(false)
    val isLoading: LiveData<Boolean> = _isLoading
      private val _error = MutableLiveData<String?>()
        val error: LiveData<String?> = _error
          private val _loginSuccess = SingleLiveEvent<Boolean>()
            val loginSuccess: LiveData<Boolean> = _loginSuccess
              // 业务逻辑
              fun login(account: String, password: String) {
              if (account.isBlank() || password.isBlank()) {
              _error.value = "账号/密码不能为空"
              return
              }
              _isLoading.value = true
              _error.value = null
              // 使用 viewModelScope,页面销毁自动取消
              viewModelScope.launch {
              delay(1200) // 模拟网络
              _isLoading.value = false
              _loginSuccess.value = true
              }
              }
              }
  2. View 层只观察,不指挥:
    class LoginActivity : AppCompatActivity() {
    private val viewModel: LoginViewModel by viewModels()
    private lateinit var binding: ActivityLoginBinding
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityLoginBinding.inflate(layoutInflater)
    setContentView(binding.root)
    // 1. 观察数据变化
    viewModel.isLoading.observe(this) { isLoading ->
    binding.progressBar.isVisible = isLoading
    binding.btnLogin.isEnabled = !isLoading
    }
    viewModel.error.observe(this) { error ->
    binding.tvTip.text = error ?: ""
    }
    viewModel.loginSuccess.observe(this) { success ->
    if (success) {
    startActivity(Intent(this, HomeActivity::class.java))
    finish()
    }
    }
    // 2. 发送事件
    binding.btnLogin.setOnClickListener {
    viewModel.login(
    binding.etAccount.text.toString(),
    binding.etPwd.text.toString()
    )
    }
    }
    }

为什么 MVVM 是行业标准?

  • 自动绑定:不需要 view.showLoading(),数据变了 UI 自动变。
  • 生命周期安全LiveData/StateFlow 能感知 Activity 生命周期,页面死了就不会再回调。
  • 代码简洁:省去大量接口定义和回调代码。

MVVM 的数据驱动思想,类似于 React 或 Vue 的响应式编程,在 JavaScript 生态中早已验证其高效性。Android 的 MVVM 结合 LiveData 或 StateFlow,让代码更加声明式。

避坑指南:MVVM 的常见误区

虽然 MVVM 很好,但在企业项目中,如果不守规矩,依然会写出烂代码。

误区一:ViewModel 持有 View 引用

// ❌ 绝对禁止
class BadViewModel : ViewModel() {
private var activity: Activity? = null // 必泄漏
}

✅ 正解:ViewModel 永远只持有数据,不持有任何 Android View 或 Context(Application Context 除外)。

误区二:在 ViewModel 里直接操作 UI

// ❌ 绝对禁止
class BadViewModel : ViewModel() {
fun login() {
Toast.makeText(getApplication(), "登录成功", Toast.LENGTH_SHORT).show() // ViewModel 不该管 Toast
}
}

✅ 正解:Toast、Dialog、Navigation 都是 View 的行为,ViewModel 只负责告诉 View “发生了什么”(通过 LiveData/Event),View 自己决定怎么展示。这类似于在 TypeScript 中保持组件纯函数,只传递状态。

误区三:状态碎片化导致不一致。当一个页面有很多状态时,如果不加约束,会出现这种情况:

// ⚠️ 危险信号
class FragmentedViewModel : ViewModel() {
val loading = MutableLiveData(false)
val data = MutableLiveData<List<Item>>()
  val error = MutableLiveData<String?>()
    val buttonEnabled = MutableLiveData(true)
    val refreshing = MutableLiveData(false)
    // 当 loading=true 时,buttonEnabled 应该是什么?没人记得,全靠运气。
    }

✅ 解决方案:当状态超过 5 个,且彼此有强关联时,请使用 MVI(单向数据流)来聚合状态。注:本篇专注于 MVVM,MVI 将在后续系列中详细展开。

[AFFILIATE_SLOT_2]

总结:如何选择?

看下表对比:

架构适用场景不建议场景
MVC个人 Demo、生命周期极短的脚本商业项目、多人协作
MVP老项目维护、对测试要求极高的模块新项目(已被 MVVM 取代)
MVVM99% 的商业项目状态极度复杂的页面(需配合 MVI)

一句话总结:MVC 是青春期的冲动,MVP 是成长的阵痛,MVVM 是成熟的代价。 请在你的新项目中,坚定不移地使用 MVVM。它结合了数据驱动、生命周期安全和简洁性,是当前 Android 开发的最佳实践。就像在 Java 或 Python 中选择成熟的框架一样,好的架构能让你专注于业务,而不是修复混乱的代码。

下期预告:系列一:架构思想进阶 | 第2篇:分层架构实战 —— 四层拆分与单向依赖。我们将讨论为什么要把代码拆成 data/domain/presentation,以及如何用 Gradle 强制禁止跨层调用。

TextViewThreadSharedPreferences