MVP-- 一个小例子彻底搞懂MVP(玉刚说)

一个小例子彻底搞懂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;@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);textView.setText((String) msg.obj);}};public void request() {new Thread(new Runnable() {@Overridepublic 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;@Overrideprotected 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() {@Overridepublic void onClick(View v) {presenter.request();}});}@Overridepublic 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() {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);callback.onResult((String) msg.obj);}};public void request() {new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic 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 BaseViewM 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<ViewModel{abstract void request1();abstract void request2();void request3() {model.getData3(new Callback3() {@Overridepublic 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 request() {return Observable.create(new ObservableOnSubscribe() {@Overridepublic void subscribe(ObservableEmitter 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() {@Overridepublic void accept(String s) throws Exception {view.updateTv(s);}}, new Consumer() {@Overridepublic 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*/@UiThreadvoid attachView(V view);/*** Will be called if the view has been destroyed. Typically this method will be invoked from* Activity.detachView() or Fragment.onDestroyView()*/@UiThreadvoid detachView(boolean retainInstance);
}

这里加@UIThread注解是为了确保attachView和detachView都运行在主线程中。
然后业务代码的活动需要继承MvpActivity

public abstract class MvpActivity<V extends MvpViewP 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.** 

Please note that only one instance of mvp delegate should be used per Activity* instance.* 

** 

* Only override this method if you really know what you are doing.* 

** @return {@link ActivityMvpDelegateImpl}*/
@NonNull protected ActivityMvpDelegate getMvpDelegate() {if (mvpDelegate == null) {mvpDelegate = new ActivityMvpDelegateImpl(thisthistrue);}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 MvpViewP 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 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 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。

 


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部