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

相关推荐
磊少工作室_CTO5 小时前
鸿蒙Next —— 状态管理实践
harmonyos·mvvm·客户端
fatiaozhang95277 小时前
中兴B860AV5.2-U_原机安卓4.4.2系统专用_晶晨S905L3SB处理器_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机·中兴b860av5.2-u
儿歌八万首7 小时前
Android 自定义 View 实战:打造一个跟随滑动的丝滑指示器
android·kotlin
我有与与症7 小时前
Kuikly 实战:手把手撸一个跨平台 AI 聊天助手 (ChatDemo)
android
恋猫de小郭7 小时前
Flutter UI 设计库解耦重构进度,官方解答未来如何适配
android·前端·flutter
apihz8 小时前
全球IP归属地查询免费API详细指南
android·服务器·网络·网络协议·tcp/ip
hgz07108 小时前
Linux环境下MySQL 5.7安装与配置完全指南
android·adb
Just_Paranoid8 小时前
【Android UI】Android 添加圆角背景和点击效果
android·ui·shape·button·textview·ripple
梁同学与Android8 小时前
Android ---【经验篇】阿里云 CentOS 服务器环境搭建 + SpringBoot项目部署(二)
android·spring boot·后端