Android中的MVP与MVVM

初学Android只晓得Activity可以控制UI,并不懂设计模式,也不知道那些操作数据的逻辑代码应该如何归类,索性都堆砌在ActivityFragment里。但随着练习的逻辑越来越复杂,Activity的代码结构开始变得混乱起来,越来越臃肿,几乎无法维护,那种在一个class里各种方法间到处跳来跳去的感觉,岂止酸爽。后来看到MVP,顿时豁然开朗,原来还可以这样,它将ViewModel彻底分离,用Presenter承上启下,让代码里每一个UI驱动的逻辑都十分清晰。

我确实喜欢MVP,也在工作中大量使用,之后又接触到Jetpack,其DataBindingLiveDataViewModel都在向我预示着一个新的设计模式:MVVM。它的ViewModel概念与MVP中一致,不同的是ViewModel,把视图和数据进行双向绑定,当数据发生变化时视图自动更新,而视图的变化也会直接作用到数据上。这种比MVP更简洁的结构让我很感兴趣。

古老的MVC

Model View Controller

mvc

  • View即视图,用于接收UI事件,控制UI状态。
  • Model即数据模型,用于处理数据,比如从数据库和网络中读写操作。
  • Controller即控制器,它接收View传来的事件,定义具体的业务实现逻辑,调用Model操作数据,然后由Model通知View更新UI,在Android中一般是ActivityFragment

通过一个简单的实例来理解MVC的结构,UI上有一个按钮,点击按钮,查询一下天气,在TextView中显示结果,并弹出一个Toast

Model

/**
 * 执行数据查询并报告结果
 */
class WeatherModel(private val callBack: Callback) {

    fun queryWeather() {
        // 从网络中查询天气
        val weather =  ...
        // 查询完成后,报告结果
        callBack.querySuccess(weather)
    }
}

/**
 * 结果回调接口
 */
interface Callback {
    fun querySuccess(weather: String)
}

View

/**
 * 接收UI点击事件,并能控制UI
 */
class WeatherView(private var activity: Activity, onBtnClickListener: OnBtnClickListener) : Callback {
    private val btn = activity.findViewById<Button>(R.id.btn)
    private val tv = activity.findViewById<TextView>(R.id.tv)

    init {
        // 收到点击事件
        btn.setOnClickListener {
            // 通知Controller执行操作
            onBtnClickListener.onClick()
        }
    }

    /**
     * UI显示天气
     */
    fun showWeather(weather: String) {
        tv.text = weather
        Toast.makeText(activity, weather, Toast.LENGTH_SHORT).show()
    }

    /**
     * Model处理完成后,调用View更新UI
     */
    override fun querySuccess(weather: String) {
        showWeather(weather)
    }
}

/**
 * 用于通知Controller点击事件的监听器
 */
interface OnBtnClickListener {
    fun onClick()
}

Controller

/**
 * 接收UI事件,控制Model处理
 */
class WeatherActivity : Activity(), OnBtnClickListener{
    private lateinit var model: WeatherModel
    private lateinit var view: WeatherView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_weather)
        view = WeatherView(this, this)
        model = WeatherModel(view)
    }

    /**
     * View被点击时,调用Model处理数据
     */
    override fun onClick() {
        model.queryWeather()
    }
}

可以看到,UI层被从Activity中剥离,Activity作为Controller持有ViewModelView要实现在有事件发生时及时通知Controller,就必须持有Controller,而Model中又持有View的一个回调接口,当UI有事件发生时,Controller得到通知,它会调用Model中的方法去处理数据,完成后,Model调用View更新UI,这是一个完整的环形结构,相互之间都有依赖,并不符合低耦合高内聚的要求。

进化的MVP

Model View Presenter

mvp

  • View即视图,用于接收UI事件,控制UI状态,一般为ActivityFragment
  • Model即数据模型,用于处理数据,比如从数据库和网络中读写操作。
  • Presenter即逻辑实现类,从View中接收事件,向下调用Model处理数据并获取结果,然后再向上控制View更新UI。

同样的例子,用MVP是这样的:

Model

/**
 * 执行数据查询并返回结果
 */
class WeatherModel(private val callBack: Callback) {

    fun queryWeather(): String {
        // 从网络中查询天气
        val weather =  ...
        // 查询完成后,返回结果
        return weather
    }
}

View

/**
 * 定义操作UI的接口
 */
interface IWeatherActivity {
    fun showWeather(weather: String)
}

/**
 * Activity实现操作UI的接口
 */
class WeatherActivity : Activity(), IWeatherActivity {
    private lateinit var iWeatherPresenter: IWeatherPresenter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_weather)
        // 创建Presenter
        iWeatherPresenter = WeatherPresenter(this)
        // Kotlin可以直接用id访问View实例
        btn.setOnClickListener {
            // 点击事件发生时,调用Presenter执行操作
            iWeatherPresenter.queryWeather()
        }
    }

    /**
     * UI显示天气
     */
    override fun showWeather(weather: String) {
        // Kotlin中可以直接用id访问View实例
        tv.text = weather
        Toast.makeText(activity, weather, Toast.LENGTH_SHORT).show()
    }
}

Presenter

/**
 * 定义响应UI事件的接口
 */
interface IWeatherPresenter {
    fun queryWeather()
}

/**
 * 实现接口,构造器要求传入View以操作UI
 */
class WeatherPresenter(private val iWeatherActivity: IWeatherActivity) : IWeatherPresenter {

    /**
     * 具体的执行点击事件
     */
    override fun queryWeather() {
        val weatherModel = WeatherModel()
        // 调用Model获取数据,然后调用View更新UI
        iWeatherActivity.showWeather(weatherModel.queryWeather())
    }
}

可以看到,在MVP模式中ViewModel是完全分离的,Activity就是View,它持有PresenterPresenter则持有ViewModel。当有事件发生时,View通知Presenter执行操作,Presenter调用Model获取数据,然后调用View更新UI。由于IPresenter接口的存在,实际上可以根据需求创建多个不同实现的Presenter实例,具有很高的灵活性。

双向绑定的MVVM

Model View ViewModel

mvvm

  • View即视图,用于接收UI事件,控制UI状态,一般为ActivityFragment
  • Model即数据模型,用于处理数据,比如从数据库和网络中读写操作。
  • ViewModel即视图模型,从View中接收事件,向下调用Model获取数据,数据被修改后,View将自动被更新。

其实MVP已经足够好了,既能实现ViewModel的分离,又能使用多Presenter实例来改变UI事件的行为。但MVCMVP都有一个共同的特点,就是UI总是是由数据驱动的,数据变化后必须使用ModelPresenter去主动更新UI,而MVVM则可以实现数据和UI的绑定,当数据变化时,UI自动更新,这一切在Android上的实现基础就是Jetpack里的DataBindingLiveData

DataBinding把数据和UI资源xml文件绑定,LiveData则允许数据感知其所在组件的生命周期,在有效的生命周期内,当数据发生变化时,UI也会随之自动改变。

同样是上面的例子,用MVVM是这样的:

Model

/**
 * 执行数据查询并返回结果
 */
class WeatherModel() {

    fun queryWeather(): String {
        // 从网络中查询天气
        val weather =  ...
        // 查询完成后,返回结果
        return weather
    }
}

View

首先在Layout资源文件中定义DataBinding

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    <!--定义DataBinding中要和View绑定的数据-->
    <data>
        <variable
            name="weather"
            type="String"/>
    </data>
    <!--正常的View布局-->
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/tv"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            <!--把内容设置为DataBinding的weather变量,
            这样点击按钮时,当数据发生变化,这里会直接随之变化-->
            android:text="@{weather}"/>
        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Show Weather"/>
    </LinearLayout>
</layout>

Activity作为View

// 注意是AppCompatActivity
class WeatherActivity : AppCompatActivity() {
    private lateinit var dataBinding: ActivityWeatherBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 使用DataBinding将视图资源加载到Activity上
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_weather)
        // 创建ViewModel,LiveData就保存在ViewModel中
        val weatherViewModel = ViewModelProviders.of(this).get(WeatherViewModel::class.java)
        // 绑定视图和数据
        dataBinding.weather = weatherViewModel.weatherLiveData.value
        // 当数据发生变化时,这里会得到通知
        weatherViewModel.weatherLiveData.observe(this, Observer<String> {
            // 弹出天气信息,注意这里的回调方法运行在LiveData发生变化的线程里
            tv.text = weatherViewModel.weatherLiveData.value
            Toast.makeText(this, weatherViewModel.weatherLiveData.value, Toast.LENGTH_SHORT).show()
        })
        // DataBinding可以直接使用id获取View实例
        dataBinding.btn.setOnClickListener {
            // 调用ViewModel执行点击操作
            weatherViewModel.queryWeather()
        }
    }
}

ViewModel

/**
 * 执行数据查询并报告结果
 */
class WeatherViewModel : ViewModel() {
    private val weatherModel = WeatherModel()
    // LiveData保存天气数据
    var weatherLiveData: MutableLiveData<String> = MutableLiveData()

    init {
        // 初始化数据
        weatherLiveData.value = ""
    }

    fun queryWeather() {
        // 从Model中查询天气
        val weather =  weatherModel.queryWeather()
        // 查询完成后,设置LiveData数据,监听此LiveData数据的观察者会得到通知
        weatherLiveData.value =  weather
    }
}

可见MVVMMVP最大的不同,当ViewModel处理好事件逻辑并更新数据后,UI是自动刷新的,而不是由Presenter主动调用View的更新视图方法。此外也可以使用DataBinding直接在视图xml文件里定义UI对数据的响应操作,实现了数据变化后UI去自动根据数据加载对应的逻辑内容,把数据和UI绑在一起就只需要在ViewModel中更新数据,而不用去管UI要怎么显示这些数据。

结语

我在工作和学习中大量使用过MVP,对MVCMVVM只是有所耳闻,接触KotlinJetpack后开始尝试在自己的练习中使用这些新东西,编程的感觉也不再是入门时枯燥的堆砌代码,而是像打造艺术品一样津津有味,这样的变化真实而有趣。

arrow_upward