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

相关推荐
魑魅魍魉都是鬼2 小时前
不练不熟,不写就忘 之 compose 之 动画之 animateSizeAsState动画练习
android·compose
一只柠檬新3 小时前
当AI开始读源码,调Bug这件事彻底变了
android·人工智能·ai编程
正经教主3 小时前
【App开发】手机投屏的几种方式(含QtScrcpy)- Android 开发新人指南
android·智能手机
-指短琴长-5 小时前
MySQL快速入门——内置函数
android·数据库·mysql
渡我白衣5 小时前
链接的迷雾:odr、弱符号与静态库的三国杀
android·java·开发语言·c++·人工智能·深度学习·神经网络
正经教主6 小时前
【App开发】02:Android Studio项目环境设置
android·ide·android studio
全栈软件开发7 小时前
最新版T5友价互站网源码商城PHP源码交易平台 完整带手机版源码网系统源码
android·开发语言·php
shykevin7 小时前
uni-app x开发商城系统,小程序发布,h5发布,安卓打包
android·小程序·uni-app
且白7 小时前
uniapp接入安卓端极光推送离线打包
android·uni-app