一个小例子彻底搞懂MVP。
https://mp.weixin.qq.com/s/Jrv6f_TU-LrzKm65wyF97w
什么是MVP
MVP全称:Model-View-Presenter; MVP是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller / Presenter负责逻辑的处理,Model提供数据,View负责显示。
为什么要使用MVP
在讨论为什么要使用MVP架构之前,我们首先要了解传统的MVC的架构的特点及其缺点。
首先看一下MVC架构的模型图,如下
MVC
这个图很简单,当查看需要更新时,首先去找控制器,然后控制器找模型获取数据,模型获取到数据之后直接更新查看。
在MVC里,View是可以直接访问Model的。从而,View里会包含模型信息,不可避免的还要包括一些业务逻辑。在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,即查看。所以,在MVC模型里,模型不依赖于View,但是查看是依依于Model的。不仅如此,因为有一些业务逻辑在查看里实现了,导致要更改查看也是比较困难的,至少那些业务逻辑是无法重用的。
这样说可能会有点抽象,下面通过一个简单的例子来说明。
假设现在有这样一个需求,活动中有一个按钮和一个TextView,点击按钮时会请求网络获取一段字符串,然后把字符串显示在TextView中,按照正常的逻辑,代码应该这么写
public class MVCActivity extends AppCompatActivity {
private Button button;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
button = findViewById(R.id.button);
textView = findViewById(R.id.text_view);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new HttpModel(textView).request();
}
});
}
}
public class HttpModel {
private TextView textView;
public HttpModel(TextView textView) {
this.textView = textView;
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
textView.setText((String) msg.obj);
}
};
public void request() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Message msg = handler.obtainMessage();
msg.obj = "从网络获取到的数据";
handler.sendMessage(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
代码很简单,当点击按钮的时候,创建一个HttpModel对象,并把TextView对象作为参数传入,然后调用它的请求方法来请求数据,当请求到数据之后,切换到主线程中更新TextView,流程完全符合上面的MVC架构图。
但是这里有个问题,首先很显然,HttpModel就是Model层,那么查看层和模型层分别干了什么事,在本例中,查看层主要做的事就是当获取到网络数据的时候,更新TextView,Controller层主要做的事就是创建HttpModel对象并调用它的请求方法,我们发现MVCActivity同时充当了View层和控制层。
这样会造成两个问题,第一,查看层和控制器层没有分离,逻辑比较混乱;第二,同样因为View和Controller层的耦合,导致活动或者Fragment很臃肿,代码量很大。由于本例比较简单,所以这两个问题都不是很明显,如果活动中的业务量很大,那么问题就会体现出来,开发和维护的成本会很高。
如何使用MVP
既然MVC有这些问题,那么应该如何改进呢,答案就是使用MVP的架构,关于MVP架构的定义前面已经说了,下面看一下它的模型图
MVP
这个图也很简单,当查看需要更新数据时,首先去找Presenter,然后Presenter去找Model请求数据,模型获取到数据之后通知Presenter,Presenter再通知查看更新数据,这样Model和View就不会直接交互了,所有的交互都由Presenter进行,Presenter充当了桥梁的角色。很显然,Presenter必须同时持有View和Model的对象的引用,才能在它们之间进行通信。
接下来用MVP的架构来改造上面的例子,代码如下
interface MVPView {
void updateTv(String text);
}
public class MVPActivity extends AppCompatActivity implements MVPView {
private Button button;
private TextView textView;
private Presenter presenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
button = findViewById(R.id.button);
textView = findViewById(R.id.text_view);
presenter = new Presenter(this);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.request();
}
});
}
@Override
public void updateTv(String text) {
textView.setText(text);
}
}
interface Callback {
void onResult(String text);
}
public class HttpModel {
private Callback callback;
public HttpModel(Callback callback) {
this.callback = callback;
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
callback.onResult((String) msg.obj);
}
};
public void request() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Message msg = handler.obtainMessage();
msg.obj = "从网络获取到的数据";
handler.sendMessage(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
public class Presenter {
private MVPView view;
private HttpModel model;
public Presenter(MVPView view) {
this.view = view;
model = new HttpModel(new Callback() {
@Override
public void onResult(String text) {
Presenter.this.view.updateTv(text);
}
});
}
public void request() {
model.request();
}
}
简单解释一下上面的代码,首先创建一个MVPView的接口,它即时查看层,里面有一个更新TextView的方法,然后让Activity实现这个接口,并复写更新TextView的方法.Model层不再传入TextView了,而是传入一个回调接口Callback,因为网络请求获取数据是异步的,在获取到数据之后需要通过Callback来通知Presenter.Presenter也很简单,首先在它的构造方法中,同时持有View和Model的引用,再对外提供一个请求方法。
分析一下上面代码执行的流程,当点击按钮的时候,Presenter调用请求方法,在它的内部,通过模型调用请求方法来请求数据,请求到数据之后,切换到主线程,调用callback的onResult方法来通知Presenter,这时候Presenter就会调用查看的更新方法来更新TextView,完成了整个流程,可以发现,在整个过程从,View和模型并没有直接交互,所有的交互都是在Presenter中进行的。
注意事项
接口的必要性
可能有的同学会问,为什么要写一个MVPView的接口,直接把活动本身传入到Presenter不行吗?这当然是可行的,这里使用接口主要是为了代码的复用,试想一下,如果直接传入活动,那么这个Presenter就只能为这一个活动服务。举个例子,假设有个App已经开发完成了,可以在手机上正常使用,现在要求做平板上的适配,在平板上的界面显示效果有所变化,TextView并不是直接在Activity中的,而是在Fragment里面,如果没有使用View的接口的话,那就需要再写一个针对Fragment的Presenter,然后把整个过程再来一遍。但是使用View的接口就很简单了,直接让Fragment实现这个接口,然后复写接口里面的方法,Presenter和Model层都不需要做任何改动。同理,Model层也可以采用接口的方式来写。
防止内存泄漏
其实上面的代码存在内存泄漏的风险。试想一下,如果在点击按钮之后,模型获取到数据之前,退出了活动,此时由于活动被演示者引用,而Presenter正在进行耗时操作,会导致活动的对象无法被回收,造成了内存泄漏,解决的方式很简单,在活动退出的时候,把Presenter对中查看的引用置为空即可。
// Presenter.java
public void detachView() {
view = null;
}
// MVPActivity.java
@Override
protected void onDestroy() {
super.onDestroy();
presenter.detachView();
}
另外还有一个问题,虽然这里活动不会内存泄漏了,但是当活动退出之后,模型中请求数据就没有意义了,所以还应该在detachView方法中,把Handler的任务取消,避免造成资源浪费,这个比较简单,就不贴代码了。
MVP的封装
很显然,MVP的实现套路是大致相同的,如果在一个应用中,存在大量的Activity和Fragment,并且都使用MVP的架构,那么难免会有很多重复工作,所以封装就很有必要性了。
在说MVP的封装之前,需要强调一点,MVP更多的是一种思想,而不是一种模式,每个开发者都可以按照自己的思路来实现具有个性化的MVP,所以不同的人写出的MVP可能会有一些差别,笔者在此仅提供一种实现思路,供读者参考。
首先模型,View和Presenter都可能会有一些通用性的操作,所以可以分别定义三个对应的底层接口。
interface BaseModel {
}
interface BaseView {
void showError(String msg);
}
public abstract class BasePresenter<V extends BaseView, M extends BaseModel> {
protected V view;
protected M model;
public BasePresenter() {
model = createModel();
}
void attachView(V view) {
this.view = view;
}
void detachView() {
this.view = null;
}
abstract M createModel();
}
这里的View层添加了一个通用的方法,显示错误信息,写在接口层,可以在实现处按照需求来显示,比如有的地方可能会是弹出一个Toast,或者有的地方需要将错误信息显示在TextView中,模型层也可以根据需要添加通用的方法,重点来看一下Presenter层。
这里的BasePresenter采用了泛型,为什么要这么做呢?主要是因为Presenter必须同时持有View和Model的引用,但是在底层接口中无法确定他们的类型,只能确定他们是BaseView和BaseModel的子类,所以采用泛型的方式来引用,就巧妙的解决了这个问题,在BasePresenter的子类中只要定义好查看和模型的类型,就会自动引用他们的对象了.Presenter中的通用的方法主要就是attachView和detachView,分别用于创建查看对象和把View的对象置位空,前面已经说过,置空是为了防止内存泄漏,Model的对象可以在Presenter的构造方法中创建。另外,这里的Presenter也可以写成接口的形式,读者可以按照自己的喜好来选择。
然后看一下在业务代码中该如何使用MVP的封装,代码如下
interface TestContract {
interface Model extends BaseModel {
void getData1(Callback1 callback1);
void getData2(Callback2 callback2);
void getData3(Callback3 callback3);
}
interface View extends BaseView {
void updateUI1();
void updateUI2();
void updateUI3();
}
abstract class Presenter extends BasePresenter<View, Model> {
abstract void request1();
abstract void request2();
void request3() {
model.getData3(new Callback3() {
@Override
public void onResult(String text) {
view.updateUI3();
}
});
}
}
}
首先定义一个契契约接口,然后把Model,View,和Presenter的子类分别放入Contract的内部,这里的一个Contract就对应一个页面(一个活动或者一个片段),放在Contract内部是为了让同一个页面的逻辑方法都放在一起,方便查看和修改.Presenter中的request3方法演示了如何通过Presenter来进行View和Model的交互。
接下来要做的就是实现这三个模块的逻辑方法了,在Activity或Fragment中实现TextContract.View的接口,再分别创建两个类用来实现TextContract.Model和TextContract.Presenter,复写里面的抽象方法就好了。
扩展:用RxJava简化代码
上面的代码中,Model层中的每个方法都传入了一个回调接口,这是因为获取数据往往是异步的,在获取的数据时需要用回调接口通知Presenter来更新查看。
如果想要避免回调接口,可以采用RxJava的方式来模型获取的数据直接返回一个Observable,接下来用RxJava的方式来改造前面的例子
public class HttpModel {
public Observable<String> request() {
return Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> emitter) throws Exception {
Thread.sleep(2000);
emitter.onNext("从网络获取到的数据");
emitter.onComplete();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}
public class Presenter {
private MVPView view;
private HttpModel model;
public Presenter(MVPView view) {
this.view = view;
model = new HttpModel();
}
private Disposable disposable;
public void request() {
disposable = model.request()
.subscribe(new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
view.updateTv(s);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
}
});
}
public void detachView() {
view = null;
if (disposable != null && !disposable.isDisposed()) {
disposable.dispose();
}
}
}
Model的请求方法直接返回一个Observable,然后在Presenter中调用subscribe方法来通知查看更新,这样就避免了使用回调接口。
开源库推荐
一个MVP架构的开源库,正如笔者所说要比较简单的集成MVP的架构,笔者推荐这个库:
https ://github.com/sockeqwe/mosby
它的使用方法比较简单,可以直接参考官方的demo,接下来简单的分析一下作者的封装思想。
首先查看层和Presenter层分别有一个基础的接口
public interface MvpView {
}
public interface MvpPresenter<V extends MvpView> {
/**
* Set or attach the view to this presenter
*/
@UiThread
void attachView(V view);
/**
* Will be called if the view has been destroyed. Typically this method will be invoked from
* <code>Activity.detachView()</code> or <code>Fragment.onDestroyView()</code>
*/
@UiThread
void detachView(boolean retainInstance);
}
这里加@UIThread注解是为了确保attachView和detachView都运行在主线程中。
然后业务代码的活动需要继承MvpActivity
public abstract class MvpActivity<V extends MvpView, P extends MvpPresenter<V>>
extends AppCompatActivity implements MvpView,
com.hannesdorfmann.mosby3.mvp.delegate.MvpDelegateCallback<V,P> {
protected ActivityMvpDelegate mvpDelegate;
protected P presenter;
protected boolean retainInstance;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getMvpDelegate().onCreate(savedInstanceState);
}
@Override protected void onDestroy() {
super.onDestroy();
getMvpDelegate().onDestroy();
}
@Override protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
getMvpDelegate().onSaveInstanceState(outState);
}
@Override protected void onPause() {
super.onPause();
getMvpDelegate().onPause();
}
@Override protected void onResume() {
super.onResume();
getMvpDelegate().onResume();
}
@Override protected void onStart() {
super.onStart();
getMvpDelegate().onStart();
}
@Override protected void onStop() {
super.onStop();
getMvpDelegate().onStop();
}
@Override protected void onRestart() {
super.onRestart();
getMvpDelegate().onRestart();
}
@Override public void onContentChanged() {
super.onContentChanged();
getMvpDelegate().onContentChanged();
}
@Override protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getMvpDelegate().onPostCreate(savedInstanceState);
}
/**
* Instantiate a presenter instance
*
* @return The {@link MvpPresenter} for this view
*/
@NonNull public abstract P createPresenter();
/**
* Get the mvp delegate. This is internally used for creating presenter, attaching and detaching
* view from presenter.
*
* <p><b>Please note that only one instance of mvp delegate should be used per Activity
* instance</b>.
* </p>
*
* <p>
* Only override this method if you really know what you are doing.
* </p>
*
* @return {@link ActivityMvpDelegateImpl}
*/
@NonNull protected ActivityMvpDelegate<V, P> getMvpDelegate() {
if (mvpDelegate == null) {
mvpDelegate = new ActivityMvpDelegateImpl(this, this, true);
}
return mvpDelegate;
}
@NonNull @Override public P getPresenter() {
return presenter;
}
@Override public void setPresenter(@NonNull P presenter) {
this.presenter = presenter;
}
@NonNull @Override public V getMvpView() {
return (V) this;
}
}
MvpActivity中持有一个ActivityMvpDelegate对象,它的实现类是ActivityMvpDelegateImpl,并且需要传入MvpDelegateCallback接口,ActivityMvpDelegateImpl的代码如下
public class ActivityMvpDelegateImpl<V extends MvpView, P extends MvpPresenter<V>>
implements ActivityMvpDelegate {
protected static final String KEY_MOSBY_VIEW_ID = "com.hannesdorfmann.mosby3.activity.mvp.id";
public static boolean DEBUG = false;
private static final String DEBUG_TAG = "ActivityMvpDelegateImpl";
private MvpDelegateCallback<V, P> delegateCallback;
protected boolean keepPresenterInstance;
protected Activity activity;
protected String mosbyViewId = null;
/**
* @param activity The Activity
* @param delegateCallback The callback
* @param keepPresenterInstance true, if the presenter instance should be kept across screen
* orientation changes. Otherwise false.
*/
public ActivityMvpDelegateImpl(@NonNull Activity activity,
@NonNull MvpDelegateCallback<V, P> delegateCallback, boolean keepPresenterInstance) {
if (activity == null) {
throw new NullPointerException("Activity is null!");
}
if (delegateCallback == null) {
throw new NullPointerException("MvpDelegateCallback is null!");
}
this.delegateCallback = delegateCallback;
this.activity = activity;
this.keepPresenterInstance = keepPresenterInstance;
}
/**
* Determines whether or not a Presenter Instance should be kept
*
* @param keepPresenterInstance true, if the delegate has enabled keep
*/
static boolean retainPresenterInstance(boolean keepPresenterInstance, Activity activity) {
return keepPresenterInstance && (activity.isChangingConfigurations()
|| !activity.isFinishing());
}
/**
* Generates the unique (mosby internal) view id and calls {@link
* MvpDelegateCallback#createPresenter()}
* to create a new presenter instance
*
* @return The new created presenter instance
*/
private P createViewIdAndCreatePresenter() {
P presenter = delegateCallback.createPresenter();
if (presenter == null) {
throw new NullPointerException(
"Presenter returned from createPresenter() is null. Activity is " + activity);
}
if (keepPresenterInstance) {
mosbyViewId = UUID.randomUUID().toString();
PresenterManager.putPresenter(activity, mosbyViewId, presenter);
}
return presenter;
}
@Override public void onCreate(Bundle bundle) {
P presenter = null;
if (bundle != null && keepPresenterInstance) {
mosbyViewId = bundle.getString(KEY_MOSBY_VIEW_ID);
if (DEBUG) {
Log.d(DEBUG_TAG,
"MosbyView ID = " + mosbyViewId + " for MvpView: " + delegateCallback.getMvpView());
}
if (mosbyViewId != null
&& (presenter = PresenterManager.getPresenter(activity, mosbyViewId)) != null) {
//
// Presenter restored from cache
//
if (DEBUG) {
Log.d(DEBUG_TAG,
"Reused presenter " + presenter + " for view " + delegateCallback.getMvpView());
}
} else {
//
// No presenter found in cache, most likely caused by process death
//
presenter = createViewIdAndCreatePresenter();
if (DEBUG) {
Log.d(DEBUG_TAG, "No presenter found although view Id was here: "
+ mosbyViewId
+ ". Most likely this was caused by a process death. New Presenter created"
+ presenter
+ " for view "
+ getMvpView());
}
}
} else {
//
// Activity starting first time, so create a new presenter
//
presenter = createViewIdAndCreatePresenter();
if (DEBUG) {
Log.d(DEBUG_TAG, "New presenter " + presenter + " for view " + getMvpView());
}
}
if (presenter == null) {
throw new IllegalStateException(
"Oops, Presenter is null. This seems to be a Mosby internal bug. Please report this issue here: https://github.com/sockeqwe/mosby/issues");
}
delegateCallback.setPresenter(presenter);
getPresenter().attachView(getMvpView());
if (DEBUG) {
Log.d(DEBUG_TAG, "View" + getMvpView() + " attached to Presenter " + presenter);
}
}
private P getPresenter() {
P presenter = delegateCallback.getPresenter();
if (presenter == null) {
throw new NullPointerException("Presenter returned from getPresenter() is null");
}
return presenter;
}
private V getMvpView() {
V view = delegateCallback.getMvpView();
if (view == null) {
throw new NullPointerException("View returned from getMvpView() is null");
}
return view;
}
@Override public void onDestroy() {
boolean retainPresenterInstance = retainPresenterInstance(keepPresenterInstance, activity);
getPresenter().detachView(retainPresenterInstance);
if (!retainPresenterInstance && mosbyViewId != null) {
PresenterManager.remove(activity, mosbyViewId);
}
if (DEBUG) {
if (retainPresenterInstance) {
Log.d(DEBUG_TAG, "View"
+ getMvpView()
+ " destroyed temporarily. View detached from presenter "
+ getPresenter());
} else {
Log.d(DEBUG_TAG, "View"
+ getMvpView()
+ " destroyed permanently. View detached permanently from presenter "
+ getPresenter());
}
}
}
@Override public void onPause() {
}
@Override public void onResume() {
}
@Override public void onStart() {
}
@Override public void onStop() {
}
@Override public void onRestart() {
}
@Override public void onContentChanged() {
}
@Override public void onSaveInstanceState(Bundle outState) {
if (keepPresenterInstance && outState != null) {
outState.putString(KEY_MOSBY_VIEW_ID, mosbyViewId);
if (DEBUG) {
Log.d(DEBUG_TAG,
"Saving MosbyViewId into Bundle. ViewId: " + mosbyViewId + " for view " + getMvpView());
}
}
}
@Override public void onPostCreate(Bundle savedInstanceState) {
}
}
代码有点长,但是逻辑还是比较清晰的,它其实就是在onCreate方法中根据不同的情况来创建Presenter对象,并通过MvpDelegateCallback的setPresenter方法把它保存在MvpDelegateCallback中,这里的MvpDelegateCallback就是MvpActivity本身。另外可以在活动的各个生命周期方法中加入需要实现的逻辑。
可能有的同学会问,为什么没有Model呢?其实这里的代码主要是对Presenter的封装,从作者给出的官方演示中可以发现,Model和View都是需要自己创建的。
这里只做一个简单的分析,有兴趣的同学可以自己查看它的源码,再强调一遍,MVP更多的是一种思想,不用局限于某一种套路,可以在领悟了它的思想之后,写出自己的MVP。