目录
- 引言
- 为什么用MVP架构
- MVP理论知识
- 乞丐版MVP架构模式的代码实现
- MVP中的代码复用场景
- 平民版MVP架构 - base层顶级父类
- Fragment怎么办
- 时尚版MVP架构 - Model层的单独优化
引言
记得第一次接触MVP开发是上大学的时候,当时看了数十篇关于MVP的文章,这里不得不吐槽一下国内技术帖子的质量真是参次不齐啊。看完之后一直懵懵懂懂的,总觉有几处关键的地方没搞清但是文章中却一带而过了,比如:
- 关于如何在Activity中高效的复用Presenter和View;
- Mode层定义到什么程度才算是比较理想的解耦;
- Model层与Presenter层如何比较优雅的相互通信。
抱着这些问题,我自己摸索着构建出了一套个性化风格MVP架构,使用过程中也优化了几次,如今一年多过去了再看这套架构也就算是个能用吧,所以决定新的架构优化。
本文讲述了MVP的核心概念和如何从最初的乞丐版MVP架构一步步升级到平民版MVP架构,时尚版MVP架构,以及即将开始更新的旗舰版MVP架构,为了保证思路清晰,文中包含大量代码与文字,跟着文中的例子便可写出一个完整的MVP架构。
为什么用MVP架构
其实我们日常开发中的Activity,Fragment和XML界面就相当于是一个 MVC 的架构模式,Activity中不仅要处理各种 UI 操作还要请求数据以及解析。
这种开发方式的缺点就是业务量大的时候一个Activity 文件分分钟飙到上千行代码,想要改一处业务逻辑光是去找就要费半天劲,而且有点地方逻辑处理是一样的无奈是不同的 Activity 就没办法很好的写成通用方法。
那 MVP 为啥好用呢?
MVP 模式将Activity 中的业务逻辑全部分离出来,让Activity 只做 UI 逻辑的处理,所有跟Android API无关的业务逻辑由 Presenter 层来完成。
将业务处理分离出来后最明显的好处就是管理方便,但是缺点就是增加了代码量。
MVP 理论知识
在MVP 架构中跟MVC类似的是同样也分为三层。
Activity 和Fragment 视为View层,负责处理 UI。
Presenter 为业务处理层,既能调用UI逻辑,又能请求数据,该层为纯Java类,不涉及任何Android API。
Model 层中包含着具体的数据请求,数据源。
三层之间调用顺序为view->presenter->model,为了调用安全着想不可反向调用!不可跨级调用!
那Model 层如何反馈给Presenter 层的呢?Presenter 又是如何操控View 层呢?看图!
上图中说明了低层的不会直接给上一层做反馈,而是通过 View 、 Callback 为上级做出了反馈,这样就解决了请求数据与更新界面的异步操作。上图中 View 和 Callback 都是以接口的形式存在的,其中 View 是经典 MVP 架构中定义的,Callback 是我自己加的。
View 中定义了 Activity 的具体操作,主要是些将请求到的数据在界面中更新之类的。
Callback 中定义了请求数据时反馈的各种状态:成功、失败、异常等。
乞丐版MVP架构模式的代码实现
下面我们用 MVP 模式构造一个简易模拟请求网络的小程序。效果图如下:
因为是模拟网络数据请求,所以有三个请求数据的按钮分别对应成功、失败、异常三种不同的反馈状态。
下面是Demo中的Java文件目录:
Callback接口
Callback 接口是Model层给Presenter层反馈请求信息的传递载体,所以需要在Callback中定义数据请求的各种反馈状态:
public interface MvpCallback { /** * 数据请求成功 * @param data 请求到的数据 */ void onSuccess(String data); /** * 使用网络API接口请求方式时,虽然已经请求成功但是由 * 于{@code msg}的原因无法正常返回数据。 */ void onFailure(String msg); /** * 请求数据失败,指在请求网络API接口请求方式时,出现无法联网、 * 缺少权限,内存泄露等原因导致无法连接到请求数据源。 */ void onError(); /** * 当请求数据结束时,无论请求结果是成功,失败或是抛出异常都会执行此方法给用户做处理,通常做网络 * 请求时可以在此处隐藏“正在加载”的等待控件 */ void onComplete(); }
Model 类
Model 类中定了具体的网络请求操作。为模拟真实的网络请求,利用postDelayed
方法模拟耗时操作,通过判断请求参数反馈不同的请求状态:
public class MvpModel { /** * 获取网络接口数据 * @param param 请求参数 * @param callback 数据回调接口 */ public static void getNetData(final String param, final MvpCallback callback){ // 利用postDelayed方法模拟网络请求数据的耗时操作 new Handler().postDelayed(new Runnable() { @Override public void run() { switch (param){ case "normal": callback.onSuccess("根据参数"+param+"的请求网络数据成功"); break; case "failure": callback.onFailure("请求失败:参数有误"); break; case "error": callback.onError(); break; } callback.onComplete(); } },2000); } }
View 接口
View接口是Activity与Presenter层的中间层,它的作用是根据具体业务的需要,为Presenter提供调用Activity中具体UI逻辑操作的方法。
public interface MvpView { /** * 显示正在加载进度框 */ void showLoading(); /** * 隐藏正在加载进度框 */ void hideLoading(); /** * 当数据请求成功后,调用此接口显示数据 * @param data 数据源 */ void showData(String data); /** * 当数据请求失败后,调用此接口提示 * @param msg 失败原因 */ void showFailureMessage(String msg); /** * 当数据请求异常,调用此接口提示 */ void showErrorMessage(); }
Presenter类
Presenter类是具体的逻辑业务处理类,该类为纯Java类,不包含任何Android API,负责请求数据,并对数据请求的反馈进行处理。
Presenter类的构造方法中有一个View接口的参数,是为了能够通过View接口通知Activity进行更新界面等操作。
public class MvpPresenter { // View接口 private MvpView mView; public MvpPresenter(MvpView view){ this.mView = view; } /** * 获取网络数据 * @param params 参数 */ public void getData(String params){ //显示正在加载进度条 mView.showLoading(); // 调用Model请求数据 MvpModel.getNetData(params, new MvpCallback() { @Override public void onSuccess(String data) { //调用view接口显示数据 mView.showData(data); } @Override public void onFailure(String msg) { //调用view接口提示失败信息 mView.showFailureMessage(msg); } @Override public void onError() { //调用view接口提示请求异常 mView.showErrorMessage(); } @Override public void onComplete() { // 隐藏正在加载进度条 mView.hideLoading(); } }); } }
xml布局文件
没什么好说的,直接上代码:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:orientation="vertical" tools:context="com.jessewu.mvpdemo.MainActivity"> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:text="点击按钮获取网络数据"/> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="获取数据【成功】" android:onClick="getData" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="获取数据【失败】" android:onClick="getDataForFailure" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="获取数据【异常】" android:onClick="getDataForError" /> </LinearLayout>
Activity
在Activity代码中需要强调的是如果想要调用Presenter就要先实现Presenter需要的对应的View接口。
public class MainActivity extends AppCompatActivity implements MvpView { //进度条 ProgressDialog progressDialog; TextView text; MvpPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (TextView)findViewById(R.id.text); // 初始化进度条 progressDialog = new ProgressDialog(this); progressDialog.setCancelable(false); progressDialog.setMessage("正在加载数据"); //初始化Presenter presenter = new MvpPresenter(this); } // button 点击事件调用方法 public void getData(View view){ presenter.getData("normal"); } // button 点击事件调用方法 public void getDataForFailure(View view){ presenter.getData("failure"); } // button 点击事件调用方法 public void getDataForError(View view){ presenter.getData("error"); } @Override public void showLoading() { if (!progressDialog.isShowing()) { progressDialog.show(); } } @Override public void hideLoading() { if (progressDialog.isShowing()) { progressDialog.dismiss(); } } @Override public void showData(String data) { text.setText(data); } @Override public void showFailureMessage(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); text.setText(msg); } @Override public void showErrorMessage() { Toast.makeText(this, "网络请求数据出现异常", Toast.LENGTH_SHORT).show(); text.setText("网络请求数据出现异常"); } }
至此,已经完整的实现了一个简易的MVP架构。
注意!以上代码中还存在很大的问题,不能用到实际开发中,下面会讨论目前存在的问题以及如何优化。
MVP中的代码复用场景
因为上节中乞丐版MVP Demo的代码只实现了一个Activity的请求操作,容易出现一个概念的混淆:
每个Activity都需要有与它对应的一套MVP(Model,View,Presenter)吗?
答案肯定是否定的!
首先不需要数据请求的Activity当然就同样不需要MVP辅助。与其他Activity中存在相同逻辑的Activity,就不需要重复生成对应的MVP。但是这个存在相同逻辑的定义,不同的场景有不同的说法:
场景1:业务逻辑完全相同
场景1中Activity A和Activity C都只有一个“买东西”的逻辑,属于典型的逻辑相同,所以Activity C就可以直接用Activity A写好的MVP无需再做任何处理。
场景2、3:包含部分相同业务逻辑
场景2和场景3的逻辑类似,都属于一个业务逻辑中包含另外一个可以单独存在的业务逻辑,这种情况采用继承的方法即可:
场景4
场景4中Activity C想要同时调用独立服务于Activity A 和 Activity B的业务逻辑,只需要将两个业务逻辑对应的Presenter分别实例化并调用业务方法即可:
private PresenterA presenterA; private PresenterB presenterB; ... ... private void getData(){ presenterA.getData(); presenterB.getData(); }
不要忘了实现两个Presenter对应的View:
public class ActivityC extends Activity implements ViewA,ViewB{ ... }
场景5
场景5属于场景3与场景4的结合体,同样需要先把A和B的业务逻辑拆分开,然后同时调用,这里就不举例子了。
总结
通过上面一揽子场景的分析,得出的第一个结论就是MVP的结构太过于繁重,所以为了避免多写重复代码和日后需要进行无意义的修改,在开发前一定要设计好逻辑调用图,这样才能事半功倍。
对于上面经典的通过业务逻辑继承实现包含重复逻辑的方法,其实也可以在一个Presenter中写好完整的逻辑方法,对于不同的Activity需要哪个业务逻辑方法就调用哪个,这样岂不就简单多了。但是从架构设计角度看这种做法是不严谨的,可能存在漏洞,所以为保持软件架构的健壮还是不要偷懒的好。
平民版MVP架构 - base层顶级父类
之前说过乞丐版MVP架构模式中还存在很多问题不能应用到实际的开发中,大概存在的问题有:
- 构架存在漏洞
- 代码冗余量大
- 通用性差
针对这些问题我们需要进一步优化,单车变摩托,升级为可以在实际开发中使用的平民版MVP架构。
调用View可能引发的空指针异常
举一个例子,在上述乞丐版MVP架构中的应用请求网络数据时需要等待后台反馈数据后更新界面,但是在请求过程中当前Activity突然因为某种原因被销毁,Presenter收到后台反馈并调用View接口处理UI逻辑时由于Activity已经被销毁,就会引发空指针异常。
想要避免这种情况的发生就需要每次调用View前都知道宿主Activity的生命状态。
之前是在Presenter的构造方法中得到View接口的引用,现在我们需要修改Presenter引用View接口的方式让View接口与宿主Activity共存亡:
public class MvpPresenter { // View接口 private MvpView mView; public MvpPresenter(){ //构造方法中不再需要View参数 } /** * 绑定view,一般在初始化中调用该方法 */ public void attachView(MvpView mvpView) { this.mView= mvpView; } /** * 断开view,一般在onDestroy中调用 */ public void detachView() { this.mView= null; } /** * 是否与View建立连接 * 每次调用业务请求的时候都要出先调用方法检查是否与View建立连接 */ public boolean isViewAttached(){ return mView!= null; } /** * 获取网络数据 * @param params 参数 */ public void getData(String params){ //显示正在加载进度条 mView.showLoading(); // 调用Model请求数据 MvpModel.getNetData(params, new MvpCallback() { @Override public void onSuccess(String data) { //调用view接口显示数据 if(isViewAttached()){ mView.showData(data); } } @Override public void onFailure(String msg) { //调用view接口提示失败信息 if(isViewAttached()){ mView.showFailureMessage(msg); } } @Override public void onError() { //调用view接口提示请求异常 if(isViewAttached()){ mView.showErrorMessage(); } } @Override public void onComplete() { // 隐藏正在加载进度条 if(isViewAttached()){ mView.hideLoading(); } } }); } }
上面Presenter代码中比之前增加了三个方法:
attachView()
绑定View引用。detachView
断开View引用。isViewAttached()
判断View引用是否存在。
其中attachView()
和detachView()
是为Activity准备的,isViewAttached()
作用是Presenter内部每次调用View接口中的方法是判断View 的引用是否存在。
把绑定View的方法写到Activity的生命周期中:
public class MainActivity extends Activity implements MvpView{ MvpPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化Presenter presenter = new MvpPresenter(); // 绑定View引用 presenter.attachView(this); } @Override protected void onDestroy() { super.onDestroy(); // 断开View引用 presenter.detachView(); } }
结合Activity构建base层
写到这里,相信大多数人都会惊讶于MVP模式代码量的巨大,冗余代码实在太多,所以接下需要为MVP中所有单元都设计一个顶级父类来减少重复的冗余代码。同样的道理,我们也为Activity设计一个父类方便与MVP架构更完美的结合。最后将所有父类单独分到一个base
包中供外界继承调用。
Callback
在乞丐版中Callback接口中的onSuccess()
方法需要根据请求数据类型的不同设置为不同类型的参数,所以每当有新的数据类型都需要新建一个Callback,解决方法是引入泛型的概念,用调用者去定义具体想要接收的数据类型:
public interface Callback<T> { /** * 数据请求成功 * @param data 请求到的数据 */ void onSuccess(T data); /** * 使用网络API接口请求方式时,虽然已经请求成功但是由 * 于{@code msg}的原因无法正常返回数据。 */ void onFailure(String msg); /** * 请求数据失败,指在请求网络API接口请求方式时,出现无法联网、 * 缺少权限,内存泄露等原因导致无法连接到请求数据源。 */ void onError(); /** * 当请求数据结束时,无论请求结果是成功,失败或是抛出异常都会执行此方法给用户做处理,通常做网络 * 请求时可以在此处隐藏“正在加载”的等待控件 */ void onComplete(); }
BaseView
View接口中定义Activity的UI逻辑。因为有很多方法几乎在每个Activity中都会用到,例如显示和隐藏正在加载进度条,显示Toast提示等,索性将这些方法变成通用的:
public interface BaseView { /** * 显示正在加载view */ void showLoading(); /** * 关闭正在加载view */ void hideLoading(); /** * 显示提示 * @param msg */ void showToast(String msg); /** * 显示请求错误提示 */ void showErr(); /** * 获取上下文 * @return 上下文 */ Context getContext(); }
BasePresenter
Presenter中可共用的代码就是对View引用的方法了,值得注意的是,上面已经定义好了BaseView
,所以我们希望Presenter中持有的View都是BaseView
的子类,这里同样需要泛型来约束:
public class BasePresenter<V extends IBaseView> { /** * 绑定的view */ private V mvpView; /** * 绑定view,一般在初始化中调用该方法 */ @Override public void attachView(V mvpView) { this.mvpView = mvpView; } /** * 断开view,一般在onDestroy中调用 */ @Override public void detachView() { this.mvpView = null; } /** * 是否与View建立连接 * 每次调用业务请求的时候都要出先调用方法检查是否与View建立连接 */ public boolean isViewAttached(){ return mvpView != null; } /** * 获取连接的view */ public V getView(){ return mvpView; }
BaseActivity
BaseActivity主要是负责实现 BaseView 中通用的UI逻辑方法,如此这些通用的方法就不用每个Activity都要去实现一遍了。
public abstract class BaseActivity extends Activity implements IBaseView { private ProgressDialog mProgressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mProgressDialog = new ProgressDialog(this); mProgressDialog.setCancelable(false); } @Override public void showLoading() { if (!mProgressDialog.isShowing()) { mProgressDialog.show(); } } @Override public void hideLoading() { if (mProgressDialog.isShowing()) { mProgressDialog.dismiss(); } } @Override public void showToast(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } @Override public void showErr() { showToast(getResources().getString(R.string.api_error_msg)); } @Override public Context getContext() { return BaseActivity.this; }
平民版MVP架构代码实现
封装好了base层我们的平民版MVP架构就完成了,下面再来实现一遍之前用乞丐版MVP实现的应用。
Model
public class MvpModel { /** * 获取网络接口数据 * @param param 请求参数 * @param callback 数据回调接口 */ public static void getNetData(final String param, final MvpCallback<String> callback){ // 利用postDelayed方法模拟网络请求数据的耗时操作 new Handler().postDelayed(new Runnable() { @Override public void run() { switch (param){ case "normal": callback.onSuccess("根据参数"+param+"的请求网络数据成功"); break; case "failure": callback.onFailure("请求失败:参数有误"); break; case "error": callback.onError(); break; } callback.onComplete(); } },2000); } }
View 接口
public interface MvpView extends BaseView{ /** * 当数据请求成功后,调用此接口显示数据 * @param data 数据源 */ void showData(String data); }
Presenter类
public class MvpPresenter extends BasePresenter<MvpView > { /** * 获取网络数据 * @param params 参数 */ public void getData(String params){ if (!isViewAttached()){ //如果没有View引用就不加载数据 return; } //显示正在加载进度条 getView().showLoading(); // 调用Model请求数据 MvpModel.getNetData(params, new MvpCallback()<String> { @Override public void onSuccess(String data) { //调用view接口显示数据 if(isViewAttached()){ getView().showData(data); } } @Override public void onFailure(String msg) { //调用view接口提示失败信息 if(isViewAttached()){ getView().showToast(msg); } } @Override public void onError() { //调用view接口提示请求异常 if(isViewAttached()){ getView().showErr(); } } @Override public void onComplete() { // 隐藏正在加载进度条 if(isViewAttached()){ getView().hideLoading(); } } }); } }
Activity
public class MainActivity extends BaseActivity implements MvpView { TextView text; MvpPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (TextView)findViewById(R.id.text); //初始化Presenter presenter = new MvpPresenter(); presenter.attachView(this); } @Override protected void onDestroy() { super.onDestroy(); //断开View引用 presenter.detachView(); } @Override public void showData(String data) { text.setText(data); } // button 点击事件调用方法 public void getData(View view){ presenter.getData("normal"); } // button 点击事件调用方法 public void getDataForFailure(View view){ presenter.getData("failure"); } // button 点击事件调用方法 public void getDataForError(View view){ presenter.getData("error"); } }
Fragment怎么办?
日常开发中,并不是所有的UI处理都在Activity中进行,Fragment也是其中很重要的一员,那么如何将Fragment结合到MVP中呢?
实现BaseFragement做法跟BaseActivity很类似,需要注意一下Fragement与宿主Activity的链接情况就可以。
public abstract class BaseFragment extends Fragment implements BaseView { public abstract int getContentViewId(); protected abstract void initAllMembersView(Bundle savedInstanceState); protected Context mContext; protected View mRootView; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mRootView = inflater.inflate(getContentViewId(), container, false); this.mContext = getActivity(); initAllMembersView(savedInstanceState); return mRootView; } @Override public void showLoading() { checkActivityAttached(); ((BaseFragmentActivity) mContext).showLoading(); } @Override public void showLoading(String msg) { checkActivityAttached(); ((BaseFragmentActivity) mContext).showLoading(msg); } @Override public void hideLoading() { checkActivityAttached(); ((BaseFragmentActivity) mContext).hideLoading(); } @Override public void showToast(String msg) { checkActivityAttached(); ((BaseFragmentActivity) mContext).showToast(msg); } @Override public void showErr() { checkActivityAttached(); ((BaseFragmentActivity) mContext).showErr(); } protected boolean isAttachedContext(){ return getActivity() != null; } /** * 检查activity连接情况 */ public void checkActivityAttached() { if (getActivity() == null) { throw new ActivityNotAttachedException(); } } public static class ActivityNotAttachedException extends RuntimeException { public ActivityNotAttachedException() { super("Fragment has disconnected from Activity ! - -."); } } }
时尚版MVP架构 - Model层的单独优化
在从乞丐版MVP架构优化成平民版MVP架构的过程中,几乎每个单元都做了很大优化并封装到了base层,但是唯独Model层没什么变化。所以,时尚版MVP架构的优化主要就是对Model层的优化。
Model层相比其他单元来说比较特殊,因为它们更像一个整体,只是单纯的帮上层拿数据而已。再就是MVP的理念是让业务逻辑互相独立,这就导致每个的网络请求也被独立成了单个Model,不光没必要这么做而且找起来贼麻烦,所以时尚版MVP架构中Model层被整体封装成了庞大且独立单一模块。
优化之后的Model层是一个庞大而且独立的模块,对外提供统一的请求数据方法与请求规则,这样做的好处有很多:
- 数据请求单独编写,无需配合上层界面测试。
- 统一管理,修改方便。
- 实现不同数据源(NetAAPI,cache,database)的无缝切换。