让代码更清晰:Android 中的 MVC、MVP 与 MVVM

真正的问题

在没有引入架构时,我们的 ActivityFragment 非常臃肿:既要负责展示 View,又要负责响应点击事件,还需要去负责网络请求,处理业务逻辑,管理数据状态...

这导致了高耦合性,这种代码几乎无法测试且难以维护。

所以我们引入了架构模式,就是为了拆开 ActivityFragment

MVC 和 MVP 的关系

首先澄清一个误区:MVP 并不是 MVC 的改进版本,只是解决同一个问题的两种不同的拆分思路。

我们通常认为原生 Android 就是 MVC,其中 Activity 是 Controller,XML 布局是 View,

kotlin 复制代码
// Model: 业务逻辑和数据
class CounterModel {
    private var count = 0
    fun increment() {
        count++
    }

    fun getCount(): Int = count
}

class MainActivity : AppCompatActivity() {
    private val model = CounterModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.view_counter_content)

        val countTextView = findViewById<TextView>(R.id.count_text_view)
        val incrementButon = findViewById<Button>(R.id.increment_button)
        incrementButon.setOnClickListener {
            model.increment()
            countTextView.text = model.getCount().toString()
        }
    }
}

view_counter_content.xml 文件布局代码:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:id="@+id/count_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp" />

    <Button
        android:id="@+id/increment_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Increment" />

</LinearLayout>

但 XML(View)只负责布局,肩负的职责太少,显示都是由 Activity(Controller)来完成的,所以 C、V 还是耦合在了一起,我们可以称其为 "M-VC" 架构。

MVC 拆 View:解耦视图

我们可以封装一个自定义 View,让它自己在管理内部的 UI 和事件,对外暴露接口和回调。

kotlin 复制代码
// Model: 业务逻辑和数据
class CounterModel {
    private var count = 0
    fun increment() {
        count++
    }

    fun getCount(): Int = count
}


// 管理自己的子视图和 UI 事件
class CounterView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {

    private val countTextView: TextView
    private val incrementButon: Button

    private var onIncrementListener: (() -> Unit)? = null

    init {
        // 加载布局
        inflate(context, R.layout.view_counter_content, this)

        countTextView = findViewById(R.id.count_text_view)
        incrementButon = findViewById(R.id.increment_button)

        incrementButon.setOnClickListener {
            // 通知 Controller 发生了点击事件
            onIncrementListener?.invoke()
        }
    }

    // 让 Controller 更新 UI
    fun setCount(count: Int) {
        countTextView.text = count.toString()
    }

    // 让 Controller 监听 UI 事件 (命名已统一)
    fun setOnIncrementListener(listener: () -> Unit) {
        this.onIncrementListener = listener
    }

}

// Controller: Activity,负责调度
class MVCActivity : AppCompatActivity() {
    private val model = CounterModel()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mvc)

        val counterView = findViewById<CounterView>(R.id.counter_view)
        // Controller 初始加载数据
        counterView.setCount(model.getCount())

        // Controller 监听 UI 事件
        counterView.setOnIncrementListener {
            // 事件触发,更新 Model
            model.increment()
            // 使用 Model 的新数据更新 UI
            counterView.setCount(model.getCount())
        }
    }
}

activity_mvc 布局代码:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<com.example.app.CounterView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/counter_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

</com.example.app.CounterView>

这样 Activity(Controller) 就很干净了,只负责调度,并不用关心控件的存在。

MVP 拆 Controller:解耦控制器

MVP 拆分的不是 View,它认为 Activity 也是 View(因为它持有 View 的引用)。

MVP 把所有的调度和业务逻辑抽取到了 Presenter 中,通过接口和 Activity 通信。

kotlin 复制代码
// Model: 业务逻辑和数据
class CounterModel {
    private var count = 0
    fun increment() {
        count++
    }

    fun getCount(): Int = count
}

// Contract: 定义 View 和 Presenter 的接口
interface CounterContract {
    // View 的接口
    interface View {
        fun showCount(count: Int)
    }

    // Presenter 的接口
    interface Presenter {
        fun loadInitialCount()
        fun incrementCount()
    }
}

// Presenter: 控制器,负责所有逻辑
class CounterPresenter(
    private val view: CounterContract.View,
    private val model: CounterModel
) : CounterContract.Presenter {

    override fun loadInitialCount() {
        view.showCount(model.getCount())
    }

    override fun incrementCount() {
        model.increment()
        view.showCount(model.getCount())
    }
}


// View: Activity,实现接口,所有逻辑委托给 Presenter
class MVPActivity : AppCompatActivity(), CounterContract.View {
    private lateinit var presenter: CounterContract.Presenter
    private lateinit var countTextView: TextView
    private lateinit var incrementButton: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mvp)

        countTextView = findViewById(R.id.count_text_view)
        incrementButton = findViewById(R.id.increment_button)
        presenter = CounterPresenter(this, CounterModel())

        // 绑定按钮点击事件到 Presenter
        incrementButton.setOnClickListener {
            presenter.incrementCount()
        }

        // 加载初始数据
        presenter.loadInitialCount()
    }

    override fun showCount(count: Int) {
        countTextView.text = count.toString()
    }
}

activity_mvp 布局文件的代码和 view_counter_content 布局一样。

Presenter 是一个纯粹的类,而 Activity 变为了被动的视图,只负责实现接口。

现在,我们就知道了 MVC 和 MVP 都是为了解耦 V 和 C,只不过两者的方向不同。MVP 并没有比 MVC 更好。

MVVM 与数据绑定

MVP 解决了耦合性,但也带来了新的问题,随着业务变得复杂,CounterContract.View 接口也会越来越臃肿,Presenter 和 View 存在了大量的命令式调用。

那么我们能不能让 View 订阅数据,我们负责修改数据,View 自动更新呢?

这就是 MVVM 的核心思想:数据绑定。

我们通常会使用 ViewModelLiveData 这两个工具来实现它:

kotlin 复制代码
// Model: 业务逻辑和数据
class CounterModel {
    private var count = 0
    fun increment() {
        count++
    }

    fun getCount(): Int = count
}

// ViewModel: 持有 Model,暴露状态
class CounterViewModel : ViewModel() {

    // ViewModel 自己创建和管理 Model
    private val model = CounterModel()

    // 对外暴露 LiveData
    private var _count = MutableLiveData<Int>()
    val count: LiveData<Int> = _count

    init {
        // 加载初始状态
        _count.value = model.getCount()
    }

    //  对外暴露业务逻辑方法
    fun incrementCount() {
        model.increment()
        _count.value = model.getCount()
    }
}


// View: Activity,持有 ViewModel
// 订阅 ViewModel 的 LiveData,调用 ViewModel 的方法
class MVVMActivity : AppCompatActivity() {

    private lateinit var countTextView: TextView
    private lateinit var incrementButton: Button
    private val viewModel: CounterViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mvvm) // 同 activity_mvp.xml

        countTextView = findViewById(R.id.count_text_view)
        incrementButton = findViewById(R.id.increment_button)

        incrementButton.setOnClickListener {
            // 调用 ViewModel 的方法
            viewModel.incrementCount()
        }

        // 订阅 ViewModel 的 LiveData
        viewModel.count.observe(this) { newCount ->
            // 状态改变时,更新 UI
            countTextView.text = newCount.toString()
        }
    }
}

Activity 不需要实现任何接口,ViewModel 也无需持有 Activity 的引用,真正实现了 V 和 VM 的解耦。

ViewModel 的角色

androidx.lifecycle.ViewModel 最初是为了解决屏幕旋转等配置变更时的数据存活问题而诞生的,无论是 Presenter 或是自定义的 VM 对象,都会在 Activity 销毁时重建。

数据绑定的本质是观察者模式,也可以通过其他方式来实现数据绑定,所以 VM 不一定代表了 androidx.lifecycle.ViewModel

但由于 ViewModel 的特性,能够持有状态、完成逻辑处理,因此,它完美地承担了 VM 的角色。

所以,现在 MVVM 架构中:

  • VM 指的一般是 ViewModel(内部持有 LiveDataStateFlow
  • V 指的是 Activity / Fragment / Composable,
  • M 是 Model(在复杂项目中通常是 RepositoryUseCase

总结

架构设计的真正核心就是拆分,无论是 MVC(拆分 View)、MVP(拆分 Controller)还是 MVVM(用数据绑定解耦)。它们的目的都是将臃肿的集合体,拆分为清晰的、各司其职的组件。

相关推荐
肯多洛夫斯基1 小时前
安卓工控屏静默连WiFi全攻略
android
极梦网络无忧1 小时前
Android无障碍服务实现抖音直播间界面监控(场控助手核心原理)
android
call me by ur name2 小时前
ERNIE 5.0 Technical Report论文解读
android·开发语言·人工智能·机器学习·ai·kotlin
kerli2 小时前
Compose 组件:Box 核心参数及其 Bias 算法
android·前端
BLUcoding3 小时前
Android 常用控件及核心属性
android
遥不可及zzz3 小时前
[特殊字符] Android AAB 一键安装工具配置指南
android·macos
私人珍藏库3 小时前
【Android】一键硬核锁手机
android·智能手机·app·工具·软件
TTTao23334 小时前
自用Android项目框架备份
android
沃尔威武4 小时前
性能调优实战:从火焰图定位到SQL优化的全流程
android·数据库·sql
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.5 小时前
基于MySQL一主一从环境添加多个新从库
android·mysql·adb