安卓应用开发中的MVC、MVP、MVVM、MVI(2)

1,417 阅读7分钟

1. MVI架构简介

MVI是Model-View-Intent的缩写,是一种最新的安卓应用开发架构模式,受到了Cycle.js框架中单向数据流和循环性质的启发。MVI与其它常见的架构模式,如MVC、MVP或MVVM,有着很大的不同。

1.1 MVI架构的组成

MVI架构由三个主要组件组成:Model、View和Intent。

  • Model:表示应用程序的状态,包括数据、用户界面和业务逻辑。Model是不可变的,只能通过Intent来改变。
  • View:负责渲染Model,并将用户操作转换为Intent。
  • Intent:表示用户或系统对应用程序状态的改变意图。Intent是唯一能够触发Model更新的方式。

1.2 MVI架构的工作流程

MVI架构遵循一个单向数据流和循环反馈机制。其工作流程如下:

  • 用户或系统触发一个事件,例如点击按钮、滑动屏幕或接收通知。
  • View将事件转换为一个Intent,并发送给Model。
  • Model接收到Intent后,根据业务逻辑处理并更新自身状态。
  • Model将更新后的状态发送给View。
  • View根据Model渲染界面。

这个过程不断重复,形成一个闭环。

2. MVI架构在安卓开发中的优势

MVI架构在安卓开发中有以下几个优势:

2.1 易于测试

由于MVI架构中Model是不可变且独立于View和Intent的,因此可以方便地对其进行单元测试。同时,由于View只负责渲染Model,并不涉及任何业务逻辑或状态管理,因此也可以轻松地对其进行UI测试。

2.2 易于调试

由于MVI架构中所有状态改变都是通过Intent来触发并记录在Model中的,因此可以方便地追踪和重现任何问题或异常。同时,由于Model是不可变且唯一确定界面显示内容和行为的源头,因此可以避免出现视图层级混乱或数据不一致等问题。

2.3 易于维护

由于MVI架构中各个组件之间有明确且简单的职责划分,并且遵循单向数据流原则,因此可以降低代码复杂度和耦合度,并提高代码可读性和可扩展性。

3. MVI架构在安卓开发中的实践

为了更好地理解和掌握MVI架构在安卓开发中如何实践,在本节我们将以一个简单而实用的计算器应用为例

3.1 创建项目和布局文件

首先,我们需要创建一个安卓项目,并在activity_main.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:orientation="vertical">

    <TextView
        android:id="@+id/tv_result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="end"
        android:textSize="32sp" />

    <GridLayout
        android:id="@+id/gl_buttons"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:columnCount="4">

        <Button
            android:id="@+id/btn_clear"
            style="@style/Widget.AppCompat.Button.Borderless.Colored"
            android:text="@string/clear" />

        <Button
            style="@style/Widget.AppCompat.Button.Borderless.Colored" />

        <Button
            style="@style/Widget.AppCompat.Button.Borderless.Colored" />

        <Button
            style="@style/Widget.AppCompat.Button.Borderless.Colored" />

        <Button
            style="@style/Widget.AppCompat.Button.Borderless.Colored" />

        <Button
            style="@style/Widget.AppCompat.Button.Borderless.Colored" />

        <Button
            style="@style/Widget.AppCompat.Button.Borderless.Colored" />

        <Button
            style="@style/Widget.AppCompat.Button.Borderless.Colored" />
        
       <!-- 省略部分按钮代码 -->

    </GridLayout>

</LinearLayout>

这段代码定义了一个垂直方向的线性布局,其中包含一个显示结果的文本视图和一个包含16个按钮的网格布局。每个按钮都有一个唯一的id和文本,分别对应数字、运算符或清除功能。

3.2 定义Model类

接下来,我们需要定义Model类,用于表示计算器应用的状态。Model类的内容如下:

// Model.kt

data class Model(
    val expression: String = "", // 表达式字符串,例如 "2+3*4-5="
    val result: String = "", // 结果字符串,例如 "9.0"
    val error: String = "" // 错误信息字符串,例如 "Invalid input."
)

这段代码定义了一个数据类Model,其中包含三个属性:expression、result和error。expression表示用户输入或显示的表达式字符串;result表示计算出或显示的结果字符串;error表示发生或显示的错误信息字符串。这三个属性都有默认值为空字符串。

3.3 定义Intent类

然后,我们需要定义Intent类,用于表示用户或系统对计算器应用状态改变的意图。Intent类是一个密封类(sealed class),其内容如下:

// Intent.kt

sealed class Intent {
    object Clear : Intent() // 清除意图,表示用户点击了清除按钮或系统初始化时触发。
    data class Append(val value: String) : Intent() // 追加意图,表示用户点击了数字或运算符按钮时触发。
    object Evaluate : Intent() // 计算意图,表示用户点击了等号按钮时触发。
}

这段代码定义了一个密封类Intent,并声明了三个子类:Clear、Append和Evaluate。Clear表示清除意图,即用户点击了清除按钮或系统初始化时触发;Append表示追加意图,即用户点击了数字或运算符按钮时触发,并携带一个value参数表示被点击按钮的文本值;Evaluate表示计算意图,即用户点击了等号按钮时触发。

3.4 定义MainActivity类

最后,我们需要定义MainActivity类,用于实现View和Intent之间的交互逻辑。MainActivity类继承MainActivity类继承自AppCompatActivity,并实现了一个名为MviView的接口。MviView接口定义了两个方法:render和intents。render方法用于根据Model渲染界面;intents方法用于返回一个Observable对象,该对象发射用户或系统产生的Intent。MainActivity类的内容如下:

// MainActivity.kt

class MainActivity : AppCompatActivity(), MviView<Model, Intent> {

    private lateinit var tvResult: TextView // 结果文本视图
    private lateinit var glButtons: GridLayout // 按钮网格布局
    private val buttons = mutableListOf<Button>() // 按钮列表
    private val calculator = Calculator() // 计算器对象,用于处理业务逻辑

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

        tvResult = findViewById(R.id.tv_result) // 初始化结果文本视图
        glButtons = findViewById(R.id.gl_buttons) // 初始化按钮网格布局

        for (i in 0 until glButtons.childCount) { // 遍历按钮网格布局中的所有子视图
            val view = glButtons.getChildAt(i) // 获取当前子视图
            if (view is Button) { // 如果子视图是按钮类型
                buttons.add(view) // 将按钮添加到按钮列表中
                view.setOnClickListener { // 设置按钮点击监听器
                    when (view.id) { // 根据按钮id判断点击事件类型
                        R.id.btn_clear -> intents().onNext(Intent.Clear) // 如果是清除按钮,发送清除意图
                        R.id.btn_equal -> intents().onNext(Intent.Evaluate) // 如果是等号按钮,发送计算意图
                        else -> intents().onNext(Intent.Append(view.text.toString())) // 否则,发送追加意图,并携带按钮文本值作为参数
                    }
                }
            }
        }

        bind() // 调用bind方法,将View和Model进行绑定

    }

    override fun render(model: Model) { 
        tvResult.text = model.expression + model.result + model.error 
        if (model.error.isNotEmpty()) {
            Toast.makeText(this, model.error, Toast.LENGTH_SHORT).show()
        }
    }

    override fun intents(): PublishSubject<Intent> {
        return PublishSubject.create()
    }

    private fun bind() {
        intents()
            .scan(Model()) { state, intent ->
                calculator.process(state, intent)
            }
            .distinctUntilChanged()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(this::render)
    }
}

这段代码定义了一个名为MainActivity的类,并实现了MviView接口。在onCreate方法中,我们初始化了结果文本视图、按钮网格布局和计算器对象,并为每个按钮设置了点击监听器。在点击监听器中,我们根据不同的事件类型,发送相应的Intent到intents方法返回的Observable对象中。然后我们调用了bind方法,将View和Model进行绑定。

在bind方法中,我们使用scan操作符对intents发射的每个Intent进行处理,并更新Model状态。然后我们使用distinctUntilChanged操作符过滤掉重复或相同的Model状态。最后我们使用observeOn操作符切换到主线程,并订阅render方法。

在render方法中,我们根据Model状态更新结果文本视图,并显示错误信息(如果有)。

4. 优缺点总结

模式优点缺点应用场景解决了哪些问题
MVC简单易懂,分工明确,有利于团队协作和维护View和Model之间的耦合度高,导致视图层逻辑复杂,难以测试适合简单的界面展示和交互实现关注点分离,提高代码可读性和复用性
MVP解决了MVC中View和Model之间的耦合问题,降低了视图层的逻辑复杂度,提高了可测试性Presenter和View之间的接口过多,增加了代码量和维护成本适合复杂的界面展示和交互实现视图和数据的解耦,提高代码可维护性和扩展性
MVVM基于数据绑定技术,进一步降低了View和ViewModel之间的耦合度,简化了代码量,提高了可维护性和可扩展性数据绑定技术有一定的学习成本,可能存在内存泄漏或性能问题适合需要频繁更新UI或者有复杂业务逻辑的界面实现双向数据绑定,提高代码可读性和用户体验
MVI基于响应式编程思想,将应用程序看作一个状态机器,将用户输入、网络请求等事件统一处理,并生成新的状态更新UI需要掌握响应式编程框架如RxJava等,状态管理可能比较复杂或冗余适合需要处理多种异步事件或者有严格要求的用户体验的界面实现单向数据流动,提高代码可预测性和稳定性

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀