让代码更清晰: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 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui