Android进阶十八 MVC、MVP、MVVM架构总结

一、MVVM概述

MVVM是一种软件开发架构,是Model-View-View Model的缩写,在Android中要实现MVVM架构,
需要使用Databinding的框架,Databinding即数据绑定,是Google为了能在Android上实现MVVM而设计的一套框架,通过它,可以进行MVVM中的ViewModel和View的单向和双向绑定,
这样View(Activity,Fragment等)只单纯的做UI显示的事情,ViewModel则用于处理逻辑,以下是一些Databinding的一些总结:

Databinding相关总结文章
Android进阶四:Databinding的使用(基础篇)
Android进阶六:Databinding的双向绑定
Android进阶十三:Databinding之@BindingConversion标签
Android进阶十四:Databinding之@BindingAdapter和Component

二、MVC、MVP、MVVM

首先,我们先大致了解Android开发中常见的模式,以便我们更深入了解MVVM 模式。

MVC

MVC,Model View Controller,是软件架构中最常见的一种框架,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示,具体见下图

这里写图片描述

View:对应于xml布局文件,显示Model层的数据结果

Model:适合做一些业务逻辑处理,比如数据库存取操作,网络操作,复杂的算法,耗时的任务等都在model层处理。

Controllor:对应于Activity业务逻辑,数据处理和UI处理

当用户出发事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。

下面看一个获取天气预报的例子:

Controller控制器

package com.xjp.androidmvcdemo.controller;

import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.xjp.androidmvcdemo.R;
import com.xjp.androidmvcdemo.entity.Weather;
import com.xjp.androidmvcdemo.entity.WeatherInfo;
import com.xjp.androidmvcdemo.model.OnWeatherListener;
import com.xjp.androidmvcdemo.model.WeatherModel;
import com.xjp.androidmvcdemo.model.WeatherModelImpl;


public class MainActivity extends ActionBarActivity implements OnWeatherListener, View.OnClickListener {

    private WeatherModel weatherModel;
    private Dialog loadingDialog;
    private EditText cityNOInput;
    private TextView city;
    private TextView cityNO;
    private TextView temp;
    private TextView wd;
    private TextView ws;
    private TextView sd;
    private TextView wse;
    private TextView time;
    private TextView njd;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        weatherModel = new WeatherModelImpl();
        initView();
    }

    /**
     * 初始化View
     */
    private void initView() {
        cityNOInput = findView(R.id.et_city_no);
        city = findView(R.id.tv_city);
        cityNO = findView(R.id.tv_city_no);
        temp = findView(R.id.tv_temp);
        wd = findView(R.id.tv_WD);
        ws = findView(R.id.tv_WS);
        sd = findView(R.id.tv_SD);
        wse = findView(R.id.tv_WSE);
        time = findView(R.id.tv_time);
        njd = findView(R.id.tv_njd);
        findView(R.id.btn_go).setOnClickListener(this);

        loadingDialog = new ProgressDialog(this);
        loadingDialog.setTitle("加载天气中...");


    }

    /**
     * 显示结果
     *
     * @param weather
     */
    public void displayResult(Weather weather) {
        WeatherInfo weatherInfo = weather.getWeatherinfo();
        city.setText(weatherInfo.getCity());
        cityNO.setText(weatherInfo.getCityid());
        temp.setText(weatherInfo.getTemp());
        wd.setText(weatherInfo.getWD());
        ws.setText(weatherInfo.getWS());
        sd.setText(weatherInfo.getSD());
        wse.setText(weatherInfo.getWSE());
        time.setText(weatherInfo.getTime());
        njd.setText(weatherInfo.getNjd());
    }

    /**
     * 隐藏进度对话框
     */
    public void hideLoadingDialog() {
        loadingDialog.dismiss();
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_go:
                loadingDialog.show();
                weatherModel.getWeather(cityNOInput.getText().toString().trim(), this);
                break;
        }
    }

    @Override
    public void onSuccess(Weather weather) {
        hideLoadingDialog();
        displayResult(weather);
    }

    @Override
    public void onError() {
        hideLoadingDialog();
        Toast.makeText(this, "获取天气信息失败", Toast.LENGTH_SHORT).show();
    }

    private <T extends View> T findView(int id) {
        return (T) findViewById(id);
    }

}

从上面代码可以看到,Activity持有了WeatherModel模型的对象,当用户有点击Button交互的时候,Activity作为Controller控制层读取View视图层EditTextView的数据,然后向Model模型发起数据请求,也就是调用WeatherModel对象的方法 getWeathre()方法。当Model模型处理数据结束后,通过接口OnWeatherListener通知View视图层数据处理完毕,View视图层该更新界面UI了。然后View视图层调用displayResult()方法更新UI。至此,整个MVC框架流程就在Activity中体现出来了。

Model模型

来看看WeatherModelImpl代码实现

package com.xjp.androidmvcdemo.model;

/**
 * Description:请求网络数据接口
 * User: xjp
 * Date: 2015/6/3
 * Time: 15:40
 */

public interface WeatherModel {
    void getWeather(String cityNumber, OnWeatherListener listener);
}

................


package com.xjp.androidmvcdemo.model;

import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.xjp.androidmvcdemo.entity.Weather;
import com.xjp.androidmvcdemo.volley.VolleyRequest;

/**
 * Description:从网络获取天气信息接口实现
 * User: xjp
 * Date: 2015/6/3
 * Time: 15:40
 */

public class WeatherModelImpl implements WeatherModel {

    @Override
    public void getWeather(String cityNumber, final OnWeatherListener listener) {

        /*数据层操作*/
        VolleyRequest.newInstance().newGsonRequest("http://www.weather.com.cn/data/sk/" + cityNumber + ".html",
                Weather.class, new Response.Listener<Weather>() {
                    @Override
                    public void onResponse(Weather weather) {
                        if (weather != null) {
                            listener.onSuccess(weather);
                        } else {
                            listener.onError();
                        }
                    }
                }, new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        listener.onError();
                    }
                });
    }
}

以上代码看出,这里设计了一个WeatherModel模型接口,然后实现了接口WeatherModelImpl类。controller控制器activity调用WeatherModelImpl类中的方法发起网络请求,然后通过实现OnWeatherListener接口来获得网络请求的结果通知View视图层更新UI 。至此,Activity就将View视图显示和Model模型数据处理隔离开了。activity担当contronller完成了model和view之间的协调作用。

至于这里为什么不直接设计成类里面的一个getWeather()方法直接请求网络数据?你考虑下这种情况:现在代码中的网络请求是使用Volley框架来实现的,如果哪天老板非要你使用Afinal框架实现网络请求,你怎么解决问题?难道是修改 getWeather()方法的实现? no no no,这样修改不仅破坏了以前的代码,而且还不利于维护, 考虑到以后代码的扩展和维护性,我们选择设计接口的方式来解决着一个问题,我们实现另外一个WeatherModelWithAfinalImpl类,继承自WeatherModel,重写里面的方法,这样不仅保留了以前的WeatherModelImpl类请求网络方式,还增加了WeatherModelWithAfinalImpl类的请求方式。Activity调用代码无需要任何修改。

问题
大家想过这样会有什么问题吗?显然是有的,不然为什么会有MVP和MVVM的诞生呢,是吧。问题就在于xml作为view层,控制能力实在太弱了,你想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,这些都没办法在xml中做,只能把代码写在activity中,造成了activity既是controller层,又是view层的这样一个窘境。大家回想一下自己写的代码,如果是一个逻辑很复杂的页面,activity或者fragment是不是动辄上千行呢?这样不仅写起来麻烦,维护起来更是噩梦。(当然看过Android源码的同学其实会发现上千行的代码不算啥,一个RecyclerView.class的代码都快上万行了呢。。)

MVC还有一个重要的缺陷,大家看上面那幅图,view层和model层是相互可知的,这意味着两层之间存在耦合,耦合对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。

正因为MVC有这样那样的缺点,所以才演化出了MVP和MVVM这两种框架。

MVP

View: 对应于Activity和xml,负责View的绘制以及与用户交互

Model: 依然是实体模型

Presenter: 负责完成View于Model间的交互和业务逻辑

MVP作为MVC的演化,解决了MVC不少的缺点,对于Android来说,MVP的model层相对于MVC是一样的,而activity和fragment不再是controller层,而是纯粹的view层,所有关于用户事件的转发全部交由presenter层处理。下面还是让我们看图:

这里写图片描述

首先MVP各部分之间的通信都是双向的,但是唯独View与Model之间是不发生联系的,二者之间的通信都是通过Presenter传递的。

MVP的要点:
1、模型与视图完全分离,我们可以修改视图而不影响模型
2、可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部
3、我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

MVP例子

model层

package com.nsu.edu.androidmvpdemo.login;

    /**
     * Created by Anthony on 2016/2/15.
     * Class Note:模拟登陆的操作的接口,实现类为LoginModelImpl.相当于MVP模式中的Model层
     */
    public interface LoginModel {
        void login(String username, String password, OnLoginFinishedListener listener);
    }
package com.nsu.edu.androidmvpdemo.login;

import android.os.Handler;
import android.text.TextUtils;
    /**
     * Created by Anthony on 2016/2/15.
     * Class Note:延时模拟登陆(2s),如果名字或者密码为空则登陆失败,否则登陆成功
     */
    public class LoginModelImpl implements LoginModel {

        @Override
        public void login(final String username, final String password, final OnLoginFinishedListener listener) {

            new Handler().postDelayed(new Runnable() {
                @Override public void run() {
                    boolean error = false;
                    if (TextUtils.isEmpty(username)){
                        listener.onUsernameError();//model层里面回调listener
                        error = true;
                    }
                    if (TextUtils.isEmpty(password)){
                        listener.onPasswordError();
                        error = true;
                    }
                    if (!error){
                        listener.onSuccess();
                    }
                }
            }, 2000);
        }
    }

View层

负责显示数据、提供友好界面跟用户交互就行。MVP下Activity和Fragment以及View的子类体现在了这一 层,Activity一般也就做加载UI视图、设置监听再交由Presenter处理的一些工作,所以也就需要持有相应Presenter的引用。本层所需要做的操作就是在每一次有相应交互的时候,调用presenter的相关方法就行。(比如说,button点击)

package com.nsu.edu.androidmvpdemo.login;

    /**
     * Created by Anthony on 2016/2/15.
     * Class Note:登陆View的接口,实现类也就是登陆的activity
     */
    public interface LoginView {
        void showProgress();

        void hideProgress();

        void setUsernameError();

        void setPasswordError();

        void navigateToHome();
    }
package com.nsu.edu.androidmvpdemo.login;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;

import com.nsu.edu.androidmvpdemo.R;

    /**
     * Created by Anthony on 2016/2/15.
     * Class Note:MVP模式中View层对应一个activity,这里是登陆的activity
     */
    public class LoginActivity extends Activity implements LoginView, View.OnClickListener {

        private ProgressBar progressBar;
        private EditText username;
        private EditText password;
        private LoginPresenter presenter;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);

            progressBar = (ProgressBar) findViewById(R.id.progress);
            username = (EditText) findViewById(R.id.username);
            password = (EditText) findViewById(R.id.password);
            findViewById(R.id.button).setOnClickListener(this);

            presenter = new LoginPresenterImpl(this);
        }

        @Override
        protected void onDestroy() {
            presenter.onDestroy();
            super.onDestroy();
        }

        @Override
        public void showProgress() {
            progressBar.setVisibility(View.VISIBLE);
        }

        @Override
        public void hideProgress() {
            progressBar.setVisibility(View.GONE);
        }

        @Override
        public void setUsernameError() {
            username.setError(getString(R.string.username_error));
        }

        @Override
        public void setPasswordError() {
            password.setError(getString(R.string.password_error));
        }

        @Override
        public void navigateToHome() {
// TODO       startActivity(new Intent(this, MainActivity.class));
            Toast.makeText(this,"login success",Toast.LENGTH_SHORT).show();
//        finish();
        }

        @Override
        public void onClick(View v) {
            presenter.validateCredentials(username.getText().toString(), password.getText().toString());
        }

    }

Presenter层

Presenter扮演着view和model的中间层的角色。获取model层的数据之后构建view层;也可以收到view层UI上的反馈命令后分发处理逻辑,交给model层做业务操作。它也可以决定View层的各种操作。

package com.nsu.edu.androidmvpdemo.login;

    /**
     * Created by Anthony on 2016/2/15.
     * Class Note:登陆的Presenter 的接口,实现类为LoginPresenterImpl,完成登陆的验证,以及销毁当前view
     */
    public interface LoginPresenter {
        void validateCredentials(String username, String password);

        void onDestroy();
    }
package com.nsu.edu.androidmvpdemo.login;

    /**
     * Created by Anthony on 2016/2/15.
     * Class Note:
     * 1 完成presenter的实现。这里面主要是Model层和View层的交互和操作。
     * 2  presenter里面还有个OnLoginFinishedListener,
     * 其在Presenter层实现,给Model层回调,更改View层的状态,
     * 确保 Model层不直接操作View层。如果没有这一接口在LoginPresenterImpl实现的话,
     * LoginPresenterImpl只 有View和Model的引用那么Model怎么把结果告诉View呢?
     */
    public class LoginPresenterImpl implements LoginPresenter, OnLoginFinishedListener {
        private LoginView loginView;
        private LoginModel loginModel;

        public LoginPresenterImpl(LoginView loginView) {
            this.loginView = loginView;
            this.loginModel = new LoginModelImpl();
        }

        @Override
        public void validateCredentials(String username, String password) {
            if (loginView != null) {
                loginView.showProgress();
            }

            loginModel.login(username, password, this);
        }

        @Override
        public void onDestroy() {
            loginView = null;
        }

        @Override
        public void onUsernameError() {
            if (loginView != null) {
                loginView.setUsernameError();
                loginView.hideProgress();
            }
        }

        @Override
        public void onPasswordError() {
            if (loginView != null) {
                loginView.setPasswordError();
                loginView.hideProgress();
            }
        }

        @Override
        public void onSuccess() {
            if (loginView != null) {
                loginView.navigateToHome();
            }
        }
    }

登陆的回调接口:

package com.nsu.edu.androidmvpdemo.login;

    /**
     * Created by Anthony on 2016/2/15.
     * Class Note:登陆事件监听
     */
    public interface OnLoginFinishedListener {

        void onUsernameError();

        void onPasswordError();

        void onSuccess();
    }

demo的代码流程:(请参考下面的类图)

  • Activity做了一些UI初始化的东西并需要实例化对应LoginPresenter的引用和实现 LoginView的接口,监听界面动作;
  • 登陆按钮按下后即接收到登陆的事件,在onClick里接收到即通过LoginPresenter的引用把它交给LoginPresenter处理。LoginPresenter接收到了登陆的逻辑就知道要登陆了;
  • 然后LoginPresenter显示进度条并且把逻辑交给我们的Model去处理,也就是这里面的LoginModel,(LoginModel的实现类LoginModelImpl),同时会把OnLoginFinishedListener也就是LoginPresenter自身传递给我们的Model(LoginModel);
  • LoginModel处理完逻辑之后,结果通过OnLoginFinishedListener回调通知LoginPresenter;
  • LoginPresenter再把结果返回给view层的Activity,最后activity显示结果
    请参考这张类图:
    这里写图片描述

注意

1 presenter里面还有个OnLoginFinishedListener,其在Presenter层实现,给Model层回调,更改View层的状态,确保 Model层不直接操作View层。

2 在一个好的架构中,model层可能只是一个领域层和业务逻辑层的入口,如果我们参考网上比较火的Uncle Bob clean architecture model层可能是一个实现业务用例的交互者,在后续的文章中应该会涉及到这方面的问题,目前能力有限。

android架构合集

MVVM

MVVM最早是由微软提出的

这里写图片描述

从图中看出,它和MVP的区别貌似不大,只不过是presenter层换成了viewmodel层,还有一点就是view层和viewmodel层是相互绑定的关系,这意味着当你更新viewmodel层的数据的时候,view层会相应的变动ui。
我们很难去说MVP和MVVM这两个MVC的变种孰优孰劣,还是要具体情况具体分析。

数据驱动

在MVVM中,以前开发模式中必须先处理业务数据,然后根据的数据变化,去获取UI的引用然后更新UI,通过也是通过UI来获取用户输入,而在MVVM中,数据和业务逻辑处于一个独立的View Model中,ViewModel只要关注数据和业务逻辑,不需要和UI或者控件打交道。由数据自动去驱动UI去自动更新UI,UI的改变又同时自动反馈到数据,数据成为主导因素,这样使得在业务逻辑处理只要关心数据,方便而且简单很多。

低耦合度

MVVM模式中,数据是独立于UI的,ViewModel只负责处理和提供数据,UI想怎么处理数据都由UI自己决定,ViewModel 不涉及任何和UI相关的事也不持有UI控件的引用,即使控件改变(TextView 换成 EditText)ViewModel 几乎不需要更改任何代码,专注自己的数据处理就可以了,如果是MVP遇到UI更改,就可能需要改变获取UI的方式,改变更新UI的接口,改变从UI上获取输入的代码,可能还需要更改访问UI对象的属性代码等等。

更新 UI

在MVVM中,我们可以在工作线程中直接修改View Model的数据(只要数据是线程安全的),剩下的数据绑定框架帮你搞定,很多事情都不需要你去关心。

团队协作

MVVM的分工是非常明显的,由于View和View Model之间是松散耦合的。一个是处理业务和数据,一个是专门的UI处理。完全有两个人分工来做,一个做UI(xml 和 Activity)一个写ViewModel,效率更高。

可复用性

一个View Model复用到多个View中,同样的一份数据,用不同的UI去做展示,对于版本迭代频繁的UI改动,只要更换View层就行,对于如果想在UI上的做AbTest 更是方便的多。

单元测试

View Model里面是数据和业务逻辑,View中关注的是UI,这样的做测试是很方便的,完全没有彼此的依赖,不管是UI的单元测试还是业务逻辑的单元测试,都是低耦合的。

通过上面对MVVM的简述和其他两种模式的对比,我们发现MVVM对比MVC和MVP来说还是存在比较大的优势,虽然目前Android开发中可能真正在使用MVVM的很少,但是是值得我们去做一些探讨和调研。

如何构建MVVM应用程序

1. 如何分工

构建MVVM框架首先要具体了解各个模块的分工,接下来我们来讲解View,ViewModel,Model 的它们各自的职责所在。

View

View层做的就是和UI相关的工作,我们只在XML和Activity或Fragment写View层的代码,View层不做和业务相关的事,也就是我们的Activity 不写和业务逻辑相关代码,也不写需要根据业务逻辑来更新UI的代码,因为更新UI通过Binding实现,更新UI在ViewModel里面做(更新绑定的数据源即可),Activity 要做的事就是初始化一些控件(如控件的颜色,添加 RecyclerView 的分割线),Activity可以更新UI,但是更新的UI必须和业务逻辑和数据是没有关系的,只是单纯的根据点击或者滑动等事件更新UI(如 根据滑动颜色渐变、根据点击隐藏等单纯UI逻辑),Activity(View层)是可以处理UI事件,但是处理的只是处理UI自己的事情,View层只处理View层的事。简单的说:View层不做任何业务逻辑、不涉及操作数据、不处理数据、UI和数据严格的分开。

ViewModel

ViewModel层做的事情刚好和View层相反,ViewModel 只做和业务逻辑和业务数据相关的事,不做任何和UI、控件相关的事,ViewModel 层不会持有任何控件的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。ViewModel就是专注于业务的逻辑处理,操作的也都是对数据进行操作,这些个数据源绑定在相应的控件上会自动去更改UI,开发者不需要关心更新UI的事情。DataBinding 框架已经支持双向绑定,这使得我们在可以通过双向绑定获取View层反馈给ViewModel层的数据,并进行操作。关于对UI控件事件的处理,我们也希望能把这些事件处理绑定到控件上,并把这些事件统一化,方便ViewModel对事件的处理和代码的美观。为此我们通过BindingAdapter 对一些常用的事件做了封装,把一个个事件封装成一个个Command,对于每个事件我们用一个ReplyCommand去处理就行了,ReplyCommand会把可能你需要的数据带给你,这使得我们处理事件的时候也只关心处理数据就行了,具体见MVVM Light Toolkit 使用指南的 Command 部分。再强调一遍ViewModel 不做和UI相关的事。

Model

Model 的职责很简单,基本就是实体模型(Bean)同时包括Retrofit 的Service ,ViewModel 可以根据Model 获取一个Bean的Observable( RxJava ),然后做一些数据转换操作和映射到ViewModel 中的一些字段,最后把这些字段绑定到View层上。

2. 如何协作

关于协作,我们先来看下面的一张图:

这里写图片描述
(图 1)

上图反应了MVVM框架中各个模块的联系和数据流的走向,由上图可知View和Model 直接是解耦的,是没有直接联系的,也就是我之前说到的View 不做任何和业务逻辑和数据处理相关的事。我们从每个模块一一拆分来看。那么我们重点就是下面的三个协作。

ViewModel与Model的协作

ViewModel与ViewModel的协作

ViewModel与View的协作

ViewModel 和View 是通过绑定的方式连接在一起的,绑定的一种是数据绑定,一种是命令绑定。数据的绑定 DataBinding 已经提供好了,简单的定义一些ObservableField就能把数据和控件绑定在一起了(如TextView的text属性),但是DataBinding框架提供的不够全面,比如说如何让一个URL绑定到一个ImageView让这个ImageView能自动去加载url指定的图片,如何把数据源和布局模板绑定到一个ListView,让ListView可以不需要去写Adapter和ViewHolder 相关的东西,而只是通过简单的绑定的方式把ViewModel的数据源绑定到Xml的控件里面就能快速的展示列表呢?这些就需要我们做一些工作和简单的封装。MVVM Light Toolkit 已经帮我们做了一部分的工作,详情可以查看MVVM Light Toolkit 使用指南。关于事件绑定也是一样,MVVM Light Toolkit 做了简单的封装,对于每个事件我们用一个ReplyCommand< T >去处理就行了,ReplyCommand< T >会把可能你需要的数据带给你,这使得我们处理事件的时候也只关心处理数据就行了。

由 图 1 中ViewModel的模块中我们可以看出ViewModel类下面一般包含下面5个部分:

  • Context (上下文)
  • Model (数据模型Bean)
  • Data Field (数据绑定)
  • Command (命令绑定)
  • Child ViewModel (子ViewModel)

我们先来看下示例代码,然后在一一讲解5个部分是干嘛用的:

    //context
    private Activity context;

    //model(数据模型Bean)
    private NewsService.News news;
    private TopNewsService.News topNews;

    //数据绑定(data field)
    public final ObservableField<String> imageUrl = new ObservableField<>();
    public final ObservableField<String> html = new ObservableField<>();
    public final ObservableField<String> title = new ObservableField<>();
    // 一个变量包含了所有关于View Style 相关的字段
    public final ViewStyle viewStyle = new ViewStyle();


    //命令绑定(command)
    public final ReplyCommand onRefreshCommand = new ReplyCommand<>(() -> {

    })
    public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((p) -> {

    });


    //Child ViewModel
    public final ObservableList<NewItemViewModel> itemViewModel = new ObservableArrayList<>();

    /** * ViewStyle 关于控件的一些属性和业务数据无关的Style 可以做一个包裹,这样代码比较美观,
     ViewModel 页面也不会有太多的字段。 **/
    public static class ViewStyle {
        public final ObservableBoolean isRefreshing = new ObservableBoolean(true);
        public final ObservableBoolean progressRefreshing = new ObservableBoolean(true);
    }

Context

Context 是干嘛用的呢,为什么每个ViewModel都最好需要持了一个Context的引用呢?ViewModel 不做和UI相关的事,不操作控件,也不更新UI,那为什么要有Context呢?原因主要有以下两点,当然也有其他用处,调用工具类、帮助类可能需要context参数等:

通过图1中,我们发现ViewModel 通过传参给Model 然后得到一个Observable,其实这就是网络请求部分,做网络请求我们必须把Retrofit Service返回的Observable绑定到Context的生命周期上,防止在请求回来时Activity已经销毁等异常,其实这个Context的目的就是把网络请求绑定到当前页面的生命周期中。

在图1中,我们可以看到两个ViewModel 之间的联系是通过Messenger来做,这个Messenger 是需要用到Context,这个我们后续会讲解。

Model

Model 是什么呢,其实就是数据原型,也就是我们用Json转过来的Java Bean,我们可能都知道,ViewModel要把数据映射到View中可能需要大量对Model的数据拷贝,拿Model 的字段去生成对应的ObservableField(我们不会直接拿Model的数据去做展示),这里其实是有必要在一个ViewModel 保留原始的Model引用,这对于我们是非常有用的,因为可能用户的某些操作和输入需要我们去改变数据源,可能我们需要把一个Bean 从列表页点击后传给详情页,可能我们需要把这个model 当做表单提交到服务器。这些都需要我们的ViewModel持有相应的model。

Data Field (数据绑定)

Data Field 就是需要绑定到控件上的ObservableField字段, 无可厚非这是ViewModel的必须品。这个没有什么好说,但是这边有一个建议:
这些字段是可以稍微做一下分类和包裹的,比如说可能一些字段绑定到控件的一些Style属性上(如果说:长度,颜色,大小)这些根据业务逻辑的变化而动态去更改的,对于着一类针对View Style的的字段可以声明一个ViewStyle类包裹起来,这样整个代码逻辑会更清晰一些,不然ViewModel里面可能字段泛滥,不易管理和阅读性较差。而对于其他一些字段,比如说title,imageUrl,name这些属于数据源类型的字段,这些字段也叫数据字段,是和业务逻辑息息相关的,这些字段可以放在一块。

Command (命令绑定)

Command (命令绑定)说白了就是对事件的处理(下拉刷新,加载更多,点击,滑动等事件处理),我们之前处理事件是拿到UI控件的引用,然后设置Listener,这些Listener 其实就是Command,但是考虑到在一个ViewModel 写各种Listener 并不美观,可能实现一个Listener就需要实现多个方法,但是我们可能只想要其中一个有用的方法实现就好了。同时实现Listener 会拿到UI的引用,可能会去做一些和UI相关的事情,这和我们之前说的ViewModel 不持有控件的引用,ViewModel不更改UI 有相悖。更重要一点是实现一个Listener 可能需要写一些UI逻辑才能最终获取我们想要的,简单一点的比如说,你想要监听ListView滑到最底部然后触发加载更多的事件,这时候你就要在ViewModel里面写一个OnScrollListener,然后在里面的onScroll方法中做计算,计算什么时候ListView滑动底部了,其实ViewModel的工作并不想去处理这些事件,它专注做的应该是业务逻辑和数据处理,如果有一个东西它不需要你自己去计算是否滑到底部,而是在滑动底部自动触发一个Command,同时把当前列表的总共的item数量返回给你,方便你通过 page=itemCount/LIMIT+1去计算出应该请求服务器哪一页的数据那该多好啊。MVVM Light Toolkit 帮你实现了这一点:

public final ReplyCommand<Integer> onLoadMoreCommand =  new ReplyCommand<>((itemCount) -> {
        int page=itemCount/LIMIT+1;
        loadData(page.LIMIT)
});

接着在XML 布局文件中通过bind:onLoadMoreCommand绑定上去就行了

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>

具体想了解更多请查看 MVVM Light Toolkit 使用指南,里面有比较详细的讲解Command的使用。当然Command并不是必须的,你完全可以依照你的习惯和喜好在ViewModel 写Listener,不过使用Command 可以使你的ViewModel 更简洁易读,你也可以自己定义更多的Command,自己定义其他功能Command,那么ViewModel的事件处理都是托管ReplyCommand来处理,这样的代码看起来会特别美观和清晰。

Child ViewModel (子ViewModel)

子ViewModel 的概念就是在ViewModel 里面嵌套其他的ViewModel,这种场景还是很常见的。比如说你一个Activity里面有两个Fragment,ViewModel 是以业务划分的,两个Fragment做的业务不一样,自然是由两个ViewModel来处理,Activity 本身可能就有个ViewModel 来做它自己的业务,这时候Activity的这个ViewModel里面可能包含了两个Fragment分别的ViewModel。这就是嵌套的子ViewModel。还有另外一种就是对于AdapterView 如ListView RecyclerView,ViewPager等。

//Child ViewModelpublic final 
ObservableList<ItemViewModel> itemViewModel = new ObservableArrayList<>();

它们的每个Item 其实就对应于一个ViewModel,然后在当前的ViewModel 通过ObservableList持有引用(如上述代码),这也是很常见的嵌套的子ViewModel。我们其实还建议,如果一个页面业务非常复杂,不要把所有逻辑都写在一个ViewModel,可以把页面做业务划分,把不同的业务放到不同的ViewModel,然后整合到一个总的ViewModel,这样做起来可以使我们的代码业务清晰,简短意赅,也方便后人的维护。

总得来说ViewModel 和View 之前仅仅只有绑定的关系,View层需要的属性和事件处理都是在xml里面绑定好了,ViewModel层不会去操作UI,只会操作数据,ViewModel只是根据业务要求处理数据,这些数据自动映射到View层控件的属性上。关于ViewModel类中包含哪些模块和字段,这个需要开发者自己去衡量,这边建议ViewModel 不要引入太多的成员变量,成员变量最好只有上面的提到的5种(context、model、…),能不进入其他类型的变量就尽量不要引进来,太多的成员变量对于整个代码结构破坏很大,后面维护的人要时刻关心成员变量什么时候被初始化,什么时候被清掉,什么时候被赋值或者改变,一个细节不小心可能就出现潜在的Bug。太多不清晰定义的成员变量又没有注释的代码是很难维护的。

我们会把UI控件的属性和事件都通过xml里面(如bind:text=@{…})绑定,但是如果一个业务逻辑要弹一个Dialog,但是你又不想在ViewModel里面做弹窗的事(ViewModel 不做UI相关的事)或者说改变ActionBar上面的图标的颜色,改变ActionBar按钮是否可点击,这些都不是写在xml里面(都是用java 初始化话),如何对这些控件的属性做绑定呢?我们先来看下代码:

public class MainViewModel implements ViewModel {
....
        //true的时候弹出Dialog,false的时候关掉dialog
        public final ObservableBoolean isShowDialog = new ObservableBoolean();
....
        .....
    }
    // 在View层做一个对isShowDialog改变的监听
    public class MainActivity extends RxBasePmsActivity {

        private MainViewModel mainViewModel;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
.....
            mainViewModel.isShowDialog.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() {
                @Override
                public void onPropertyChanged(android.databinding.Observable sender, int propertyId) {
                    if (mainViewModel.isShowDialog.get()) {
                        dialog.show();
                    } else {
                        dialog.dismiss();
                    }
                }
            });
        }
 ...
    }

简单的说你可以对任意的ObservableField做监听,然后根据数据的变化做相应UI的改变,业务层ViewModel 只要根据业务处理数据就行,以数据来驱动UI。

ViewModel与Model的协作

从图1 中,Model 是通过Retrofit 去获取网络数据的,返回的数据是一个Observable< Bean >( RxJava ),Model 层其实做的就是这些。那么ViewModel 做的就是通过传参数到Model层获取到网络数据(数据库同理)然后把Model的部分数据映射到ViewModel的一些字段(ObservableField),并在ViewModel 保留这个Model的引用,我们来看下这一块的大致代码(代码涉及到简单RxJava,如看不懂可以查阅入门一下):

//Model
    private NewsDetail newsDetail;

    private void loadData(long id) {
        //  Observable<Bean> 用来获取网络数据
        Observable<Notification<NewsDetailService.NewsDetail>>   newsDetailOb =
                RetrofitProvider.getInstance()
                        .create(NewsDetailService.class)
                        .getNewsDetail(id)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        // 将网络请求绑定到Activity 的生命周期
                        .compose(((ActivityLifecycleProvider) context).bindToLifecycle())
                        //变成 Notification<Bean> 使我们更方便处理数据和错误
                        .materialize().share();

        // 处理返回的数据
        newsDetailOb.filter(Notification::isOnNext)
                .map(n -> n.getValue())
                // 给成员变量newsDetail 赋值,之前提到的5种变量类型中的一种(model类型)        
                .doOnNext(m -> newsDetail = m)
                .subscribe(m -> initViewModelField(m));

        // 网络请求错误处理
        NewsListHelper.dealWithResponseError(
                newsDetailOb.filter(Notification::isOnError)
                        .map(n -> n.getThrowable()));
    }
    //Model -->ViewModel
    private void initViewModelField(NewsDetail newsDetail) {
        viewStyle.isRefreshing.set(false);
        imageUrl.set(newsDetail.getImage());
        Observable.just(newsDetail.getBody())
                .map(s -> s + "<style type=\"text/css\">" + newsDetail.getCssStr())
                .map(s -> s + "</style>")
                .subscribe(s -> html.set(s));
        title.set(newsDetail.getTitle());
    }

以上代码基本把注释补全了,基本思路比较清晰,,Rxjava涉及的操作符都是比较基本的,如有不懂,可以稍微去入门,之后的源码里面ViewModel数据逻辑处理都是用Rxjava做,所以需要提前学习一下方便你看懂源码。

注:我们推荐使用MVVM 和 RxJava一块使用,虽然两者皆有观察者模式的概念,但是我们RxJava不使用在针对View的监听,更多是业务数据流的转换和处理。DataBinding框架其实是专用于View-ViewModel的动态绑定的,它使得我们的ViewModel 只需要关注数据,而RxJava 提供的强大数据流转换函数刚好可以用来处理ViewModel中的种种数据,得到很好的用武之地,同时加上Lambda表达式结合的链式编程,使ViewModel 的代码非常简洁同时易读易懂。

ViewModel与ViewModel的协作

在图 1 中 我们看到两个ViewModel 之间用一条虚线连接着,中间写着Messenger,Messenger 可以理解是一个全局消息通道,引入messenger最主要的目的就实现ViewModel和ViewModel的通信,也可以用做View和ViewModel的通信,但是并不推荐这样做。ViewModel主要是用来处理业务和数据的,每个ViewModel都有相应的业务职责,但是在业务复杂的情况下,可能存在交叉业务,这时候就需要ViewModel和ViewModel交换数据和通信,这时候一个全局的消息通道就很重要的。关于Messenger 的详细使用方法可以参照 MVVM Light Toolkit 使用指南的 Messenger 部分,这边给出一个简单的例子仅供参考:
场景是这样的,你的MainActivity对应一个MainViewModel,MainActivity 里面除了自己的内容还包含一个Fragment,这个Fragment 的业务处理对应于一个FragmentViewModel,FragmentViewModel请求服务器并获取数据,刚好这个数据MainViewModel也需要用到,我们不可能在MainViewModel重新请求数据,这样不太合理,这时候就需要把数据传给MainViewModel,那么应该怎么传,彼此没有引用或者回调。那么只能通过全局的消息通道Messenger。
FragmentViewModel 获取消息后通知MainViewModel 并把数据传给它:

   combineRequestOb.filter(Notification::isOnNext)
            .map(n -> n.getValue())
            .map(p -> p.first)
            .filter(m -> !m.getTop_stories().isEmpty())
            .doOnNext(m ->Observable.just(NewsListHelper.isTomorrow(date)).filter(b -> b).subscribe(b -> itemViewModel.clear()))
// 上面的代码可以不看,就是获取网络数据 ,通过send把数据传过去
            .subscribe(m -> Messenger.getDefault().send(m, TOKEN_TOP_NEWS_FINISH));

MainViewModel 接收消息并处理:

Messenger.getDefault().register(activity, NewsViewModel.TOKEN_TOP_NEWS_FINISH, TopNewsService.News.class, (news) -> {
// to something....
    }

在MainActivity onDestroy 取消注册就行了(不然导致内存泄露):

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Messenger.getDefault().unregister(this);
    }

当然上面的例子也只是简单的说明下,Messenger可以用在很多场景,通知,广播都可以,不一定要传数据,在一定条件下也可以用在View层和ViewModel 上的通信和广播。运用范围特别广,需要开发者结合实际的业务中去做更深层次的挖掘。

猜你喜欢

转载自blog.csdn.net/lixpjita39/article/details/79330187