让代码更清晰: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(用数据绑定解耦)。它们的目的都是将臃肿的集合体,拆分为清晰的、各司其职的组件。

相关推荐
s***11702 小时前
Mysql convert函数、convert用法、字符串转数字、字符串转日期、类型转换函数
android·数据库·mysql
n***26563 小时前
【MySQL】MVCC详解, 图文并茂简单易懂
android·数据库·mysql
程序猿陌名!3 小时前
Android-EDLA RK3576谷歌ATTESTION-KEY从申请到烧录以及验证谷歌认证标志全流程
android
安卓理事人3 小时前
安卓版本升级功能
android
s***35303 小时前
怎么下载安装yarn
android·前端·后端
z***94843 小时前
使用rustDesk搭建私有远程桌面
android·前端·后端
q***06293 小时前
【细如狗】记录一次使用MySQL的Binlog进行数据回滚的完整流程
android·数据库·mysql
0***86333 小时前
图文详述:MySQL的下载、安装、配置、使用
android·mysql·adb
9***44633 小时前
SQLyog安装配置(注册码)连接MySQL
android·mysql·adb
o***11143 小时前
【MySQL】MySQL库的操作
android·数据库·mysql