对Android中MVP的理解

软件发展这么多年,诞生出了很多有名的架构思想,小到设计模式,大到开发框架。当然,本篇文章要说的MVP,它既不是模式也不是框架,而是一种构建项目的架构思想。

那么有同学会问了,我为什么要用这些“多余”的,甚至可以说是繁琐的设计模式、框架或者架构呢。按常规套路,直观的开发模式不是很好么,而且很容易理解。

如果需要开发的应用比较简单,几个类加上一些开源的库就能搞定,那么就没必要整什么架构或者设计模式了,切忌为了模式而模式,为了架构而架构,那样就跟扛着大炮去打蚊子了一样,累死不说,效率还低的吓人。

然而在大型的面向大众的应用中,项目需求不断的更新迭代,引入架构的目的是为了简化代码的逻辑、便于协作开发、方便后期维护,说的高大上一点就是为了实现模块内部的高聚合和模块之间的低耦合。下面开始步入正题。

说到MVP就不得不提MVC了,MVP模式是由MVC模式延生而来的。先来看看MVC模式在Android项目中是如何体现的。

1、View层:一般对应xml中的布局。

2、Model层:比方说数据库存取操作,网络操作等。

3、Controller层:处理用户交互,比方说按钮的点击,手势的缩放滑动等等,向Model层发送请求,处理Model层的回调等。

无论小项目还是大项目,大部分同学在刚开始接触Android的时候基本都是采用类似的开发套路。

下面以实际案例来分析下MVC架构的应用。假设有这样一个需求,根据输入的城市编码获取城市天气信息。先来分析下这个需求,要求输入的地方肯定在View中处理,监听并发起请求在Controller中处理,获取城市天气在Model中处理,然后获取到数据后在Controller中拿到数据并处理之。下面用伪代码来模拟实际开发流程。

weather_activity.xml

<LinearLayout>
    <EditText />
    <Button />
</LinearLayout/>

WeatherActivity.java

class WeatherActivity extends Activity {
    onCreate() {
    	EditText et = findViewById();
    	Button btn = findViewById();
    	
    	btn.setOnClickListener(new OnClickListener() {
    		String code = et.getText();
    		getData(code);
    	});
    }
    getData(String code) {
    	Map<String, String> params = new HashMap<>();
    	params.put("code", code);
    	HttpUtil.get(params, new Callback() {
    		void onSuccess(JSONObject json) {
    			//处理数据并初始化界面
    		}
    		
    		void onError(Exception e) {
    		}
    	});
    }
}

Callback.java

interface Callback {
    void onSuccess(JSONObject json);
    void onError(Exception e);
}

HttpUtil.java

class HttpUtil {
    static get(Map<String, String> params, Callback callback) {
        OkHttpClient.get(params, new Listener() {
            void onResponse(JSONObject json) {
                callback.onSuccess(json);
            }
            
            void onError(Exception e) {
                callback.onError(e);
            }
        });
    }
}

你看MVC架构用的不是好得很吗,为什么还要用MVP呢?

传统的MVC架构是Controller起到中间桥梁的作用,将Model和View隔离开来。而反过来再看下我们都做了什么?HttpUtil作为Model层,main.xml作为View层,MainActivity作为Controller层,而且Controller也确实做到了隔断M和V的目的,但是不知道各位有没有发现,MainActivity里面做的工作实在是太多了,上面示例很简单,但是在实际项目开发中,随着一个页面的逻辑复杂度不断提升,那么Activity中需要做的工作就会越来越多,以致变得庞大臃肿,一些复杂的页面,几千行代码写在一个Activity都很常见。这样不仅写起来异常麻烦,维护起来简直就是噩梦。而且相比较传统的MVC架构,在Android中,V和C区分的并不太明显,甚至可以说V和C都柔和到一起了。在Controller(Activity)中不仅要初始化界面,接收用户操作并作出响应,还要进行复杂的逻辑控制。

上面示例代码中介绍的开发思想适合运用在一些小项目并且是不需要单元测试的项目中,而一旦在大型的项目开发中,如果还是用这种开发模式的话,抛开上面提到的Activity臃肿的问题不说,测试人员测试接口是否通畅,mock数据都变得异常的复杂。

网上看到一篇博客也是介绍MVC在Android中的应用,这篇文章大家可以去看看,下面就根据这位老铁提供的思路改造下我们上面的示例代码。

IWeatherModel.java

interface IWeather {
    void getData(String code, OnViewListener l);
}

OnViewListener.java

interface OnViewListener {
    void onSuccess(Weather w);
}

WeatherModelImpl.java

class WeatherModelImpl implements IWeatherModel {
    void getData(String code, OnViewListener l) {
        Map<String, String> params = new HashMap<>();
    	params.put("code", code);
    	HttpUtil.get(params, new Callback() {
    		void onSuccess(JSONObject json) {
    			//解析数据得到Weather对象
    			l.onSuccess(weather);
    		}
    		
    		void onError(Exception e) {
    		}
    	});
    }
}

WeatherActivity.java

class WeatherActivity extends Activity implements OnViewListener {
    IWeatherModel w;
    onCreate() {
    
    	w = new WeatherModelImpl();
    
    	EditText et = findViewById();
    	Button btn = findViewById();
    	
    	btn.setOnClickListener(new OnClickListener() {
    		String code = et.getText();
    		getData(code);
    	});
    }
    
    @Override
    void onSuccess(Weather w) {
    	
    }
    
    getData(String code) {
    	w.getData(code, this);
    }
}

作者的做法和我的做法不同的地方就是他另外针对每个模块制定了一个接口,再去实现接口,该接口定义了Activity和Model交互的所有方法。这种实现的思路跟接下来我要说的MVP思想很类似,只不过更像是将MVP模式中M和P结合到一起的产物。这种写法的好处我觉得有以下几点:

1、接口的方式去实现Model,降低了Activity和Model的耦合度,正如作者说的那样,万一哪天需要替换网络加载框架,可以在不改变原有代码的情况去新增另外一个网络加载框架的实现类。

2、对测试来说也比较容易,可以不用实现一个Activity类来测试,只需要写一个java类实现IWeather接口就可以测试网络请求数据是否正确,另外也可以在WeatherImpl中mock数据来测试view的显示是否正确。

3、相较于第一种我的那种实现方式,将数据的处理部分都挪到了Model实现类中去做,解放了Activity,Activity只需要协调好View和Model就好了,它自己实际上还是既充当了View(因为view的初始化,赋值,控制等操作还是在Activity中执行)又充当了Controller,只不过更多的是充当了中间协调者的角色。

但是缺点有么?很显然是有的,小项目是看不出来,尤其是在大项目中,涉及到很多业务逻辑的处理,数据库操作等等,那么这些逻辑处理的代码甚至是控制代码都需要放到Model实现类中去写,这样随着业务逻辑复杂度不断提升,同样会造成Model臃肿的情况。那么怎么解决这种问题呢?答案是:再将Model分离!

聪明的工程师们于是乎将MVP思想应用到了Android项目开发中,以解决上面MVC带来的问题,说白了MVP的出现就是为了让Activity或Model得到解放,而MVP的思想也就是将上面第二种实现方式中的Model分离开来以M和P的姿态出现!

关于MVP架构谷歌给我们提供了一个很好的例子示范,接下来根据官方的示例作介绍,然后结合目前主流的MVP+Retrofit+RxJava框架来写一个简单的例子。先看下官方的示例程序的结构:

简单分析下,data目录下类的作用是获取本地数据和网络数据也就是Model,util目录存放工具类,BasePresenter和BaseView分别是基类,里面定义了最基础的方法,可以认为是所有模块必须要实现的方法。

BasePresenter.java

public interface BasePresenter {

    void start();

}

BaseView.java

public interface BaseView<T> {

    void setPresenter(T presenter);

}

其余目录对应项目中具体的模块,这里也给我们一个很好的示范,在实际项目开发中我也建议大家将项目分解成各个不同的模块,一般情况下我们可以根据项目中页面来定模块,比方说login登录模块,home首页模块等。这些模块都对应有ModuleActivity、ModuleContract、ModuleFragment、ModulePresenter四个文件,官方示例实际上是将Fragment作为View的,Activity只是起到初始化的作用,初始化Fragment和Presenter。

我们看下AddEditTaskActivity类节选

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);
        AddEditTaskFragment addEditTaskFragment = (AddEditTaskFragment) getSupportFragmentManager()
                .findFragmentById(R.id.contentFrame);
        ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
                    addEditTaskFragment, R.id.contentFrame);
        // Create the presenter
        mAddEditTaskPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskFragment,
                shouldLoadDataFromRepo);
}

我们再看下AddEditTaskPresenter类节选

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {
        
        mAddTaskView.setPresenter(this);
}

实际上整个页面的初始化,逻辑控制,发起请求等都在Fragment做的。我在实际项目开发中并没有将View分类,除非必须要用Fragment,要不然我都是将Activity作为View的。接下来我就根据官方提供的MVP架构的思路来写一个简单的例子。还是以根据城市编码获取天气的作为需求。

先看下项目的目录结构

weather_activity.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="horizontal">

    <EditText
        android:id="@+id/codeEt"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="获取天气"/>

</LinearLayout>

Weather.java

public class Weather {

    //这里定义天气相关的属性
}

WeatherContract.java

public interface WeatherContract {
    interface IView {
        void onSuccess(Weather weather);
        void onError(Exception e);
    }

    interface IPresenter {
        void getData(String code);
    }

    interface IModel {
        interface NetworkCallback {
            void onNetworkSuccess(JSONObject json);
            void onNetworkError(Exception e);
        }

        void getData(Map<String, String> params, NetworkCallback callback);
    }
}

WeatherModel.java

public class WeatherModel implements WeatherContract.IModel {
    @Override
    public void getData(Map<String, String> params, NetworkCallback callback) {
        //这里可以使用okhttp或者volley来获取网络数据
        callback.onNetworkSuccess(new JSONObject());
    }
}

WeatherPresenter.java

public class WeatherPresenter implements WeatherContract.IPresenter, WeatherContract.IModel.NetworkCallback {

    WeatherContract.IView mView;
    WeatherContract.IModel mModel;

    public WeatherPresenter(WeatherContract.IView view) {
        mView = view;
        mModel = new WeatherModel();
    }

    @Override
    public void getData(String code) {
        Map<String, String> params = new HashMap<>();
        params.put("code", code);
        mModel.getData(params, this);
    }

    @Override
    public void onNetworkSuccess(JSONObject json) {
        //解析数据
        mView.onSuccess(new Weather());
    }

    @Override
    public void onNetworkError(Exception e) {
        mView.onError(e);
    }
}

WeatherActivity.java

public class WeatherActivity extends AppCompatActivity implements WeatherContract.IView {

    private EditText mCodeEt;
    private Button mBtn;

    private WeatherContract.IPresenter mPresenter;

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

        mPresenter = new WeatherPresenter(this);

        initView();
    }

    private void initView() {
        mCodeEt = findViewById(R.id.codeEt);
        mBtn = findViewById(R.id.btn);

        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String code = mCodeEt.getText().toString();
                getData(code);
            }
        });
    }

    private void getData(String code) {
        mPresenter.getData(code);
    }

    @Override
    public void onSuccess(Weather weather) {
        //初始化界面
    }

    @Override
    public void onError(Exception e) {

    }
}

可以看到Activity作为真正的View,没有参与任何逻辑控制,当然实际项目开发中看实际情况,可以适当的加入一些逻辑控制代码,但是获取数据源的操作都是通过Presenter来直接发起,并通过Model来直接获取,Activity只是对返回的数据和View绑定而已。对比上面说的第二种MVC的实现方式,Presenter充当的角色实际上是Model和View之前的桥梁,通过Presenter我们发起网络请求,并对返回的数据进一步处理,处理完成后通过接口的形式返回给View也就是Activity来使用。

这里是MVP代码 。

关于MVP和rxjava、retrofit的结合,网上很多博客介绍的都很清楚,我这里简单的说下思路,主要是将Model中的数据访问部分换成Retrofit,Retrofit本身就有对rxjava的封装,返回的数据类型是Observable类型,然后在Presenter中对返回的数据做进一步处理。下面是我的封装方式。仅供参考,大神勿喷。

同样的,先来看下项目目录结构

BaseEntity.java

package com.ldw.mrr;

import android.text.TextUtils;

import com.google.gson.annotations.SerializedName;

/**
 * Created by www.longdw.com on 2017/11/24 下午1:58.
 */

public class BaseEntity<E> {
    public static String TAG;

    @SerializedName("status_code") public String statusCode;
    @SerializedName("msg") public String msg;
    @SerializedName("hasnext") public String hasnext;

    @SerializedName("data") public E data;

    public static String SUCCESS = "200";
    public static String SIGN_OUT = "30000";//token验证失败

    public BaseEntity() {
        TAG = this.getClass().getName();
    }

    public boolean isSuccess() {
        return SUCCESS.equals(statusCode);
    }

    public boolean isTokenInvalid() {
        return SIGN_OUT.equals(statusCode);
    }

    public String getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(String statusCode) {
        this.statusCode = statusCode;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getHasnext() {
        return hasnext;
    }

    public void setHasnext(String hasnext) {
        this.hasnext = hasnext;
    }

    public boolean hasNext() {
        return !TextUtils.isEmpty(hasnext) && hasnext.equals("1");
    }

    public E getData() {
        return data;
    }

    public void setData(E data) {
        this.data = data;
    }
}

BaseMVP.java

package com.ldw.mrr;

import android.content.Context;

import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;

/**
 * 管理MVP接口
 * Created by www.longdw.com on 2017/11/29 下午4:14.
 */
public interface BaseMVP {

    interface IBaseView<D> {
        void onRefreshSuccess(BaseEntity<D> entity);
        void onRefreshError(Throwable e);
        void onError(BaseEntity entity);
    }

    interface IBaseLoadView<D> extends IBaseView<D> {
        void onLoadSuccess(BaseEntity<D> entity);
        void onLoadError(Throwable e);
    }


    class BasePresenter<V extends IBaseView> {
        protected String TAG;
        protected V mView;
        protected Context mContext;

        //将所有正在处理的Subscription都添加到CompositeSubscription中。统一退出的时候注销观察
        private CompositeDisposable mCompositeDisposable;

        public BasePresenter(Context context) {
            mContext = context;
            TAG = this.getClass().getName();
        }

        public void attach(V view) {
            mView = view;
        }

        public void detach() {
            this.mView = null;
            this.mContext = null;

            unDisposable();
        }

        public void onResume() {
        }

        public void addDisposable(Disposable subscription) {
            if (mCompositeDisposable == null || mCompositeDisposable.isDisposed()) {
                mCompositeDisposable = new CompositeDisposable();
            }
            mCompositeDisposable.add(subscription);
        }

        /**
         * 在界面退出等需要解绑观察者的情况下调用此方法统一解绑,防止Rx造成的内存泄漏
         */
        public void unDisposable() {
            if (mCompositeDisposable != null) {
                mCompositeDisposable.dispose();
            }
        }
    }

    class BaseLoadPresenter<V extends IBaseLoadView> extends BasePresenter<V> {
        private static final int PAGE_SIZE = 6;

        protected int mPageIndex = 1;
        protected int mPageSize = PAGE_SIZE;

        public BaseLoadPresenter(Context context) {
            super(context);
        }
    }
}

WeatherContract.java

public interface WeatherContract {
    interface IView extends BaseMVP.IBaseView<Weather> {

    }

    abstract class APresenter extends BaseMVP.BasePresenter<IView> {

        public APresenter(Context context) {
            super(context);
        }

        public abstract void requestData(String code);
    }

    interface IModel {
        Observable<BaseEntity<Weather>> requestData(Map<String, String> params);
    }
}

WeatherModel.java

/**
 * Created by www.longdw.com on 2018/3/16 下午4:03.
 */

public class WeatherModel implements WeatherContract.IModel {

    @Override
    public Observable<BaseEntity<Weather>> requestData(Map<String, String> params) {
        return RetrofitFactory.getInstance(HttpConstants.HOST).getData(params).subscribeOn(Schedulers.io()).observeOn
                (AndroidSchedulers.mainThread());
    }
}

WeatherPresenter.java

/**
 * Created by www.longdw.com on 2018/3/16 下午4:05.
 */

public class WeatherPresenter extends WeatherContract.APresenter {

    WeatherContract.IModel mModel;

    public WeatherPresenter(Context context) {
        super(context);
        mModel = new WeatherModel();
    }

    @Override
    public void requestData(String code) {
        Map<String, String> params = new HashMap<>();
        params.put("code", code);
        mModel.requestData(params).subscribe(new Observer<BaseEntity<Weather>>() {
            @Override
            public void onSubscribe(Disposable d) {
                addDisposable(d);
            }

            @Override
            public void onNext(BaseEntity<Weather> weatherBaseEntity) {
                mView.onRefreshSuccess(weatherBaseEntity);
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });
    }
}

这里是项目源码

跟第一种MVP实现方式不同的地方是Presenter我采用的是抽象类的方式,其实跟接口没什么本质的区别,主要是为了方便Presenter的实现类的使用,省去了既要实现接口又要集成BasePresenter。

最后再说几句,不论是MVC还是MVP思想,每个人都有自己的理解和实现方式,找到适合自己的才是最重要的。另外还是那句话,不要为了设计而设计,为了架构而架构,根据实际情况去考虑要不要选择架构,选择什么样的架构。