目录
- 架构设计的目的
- 对 MVP 的理解
- 代码
- Model
- View
- Presenter
- Android 中 MVP 的问题
- 试吃个小李子
- Model
- View
- Presenter
- 效果展示
大家好!
作为 Android 程序猿,你有研究过 MVP 架构吗?在开始接触 Android 那一刻起,我们就开始接触 MVC 架构,可谓是用的不亦乐乎。可为什么又出现了 MVP 呢?都说它比 MVC 好,到底又好在哪里呢?
架构设计的目的
通过设计使程序模块化,模块内 高内聚、模块间 低耦合,提高开发效率,便于复用及后续维护。
对 MVP 的理解
上图是 MVP 的架构图,我们都知道,MVP架构中 M 代表 Model(模型)、V 代表 View(视图)、P 代表 Presenter(主持人/控制器)。它们的职责分别是:
- View 负责接收用户的输入事件,然后将事件传递给 Presenter;
- Presenter 收到事件后,会进行业务分发,通知 Model 获取数据;
- Model 区分数据来源,进而通过不同渠道获取数据,拿到数据后返回给 Presenter;
- Presenter 进行后续处理,或者通知 View 更新 UI。
相比 MVC 架构,MVP 架构看上去就清晰了很多:事件由 View 流向 Presenter 流向 Model,然后再由 Model 流回 Presenter 流回 View。在 MVP 架构中,Activity 就全心的处理着和 View 相关的事情,Model 负责向下分发数据请求,替 Presenter 分担了很大一部分负担,这里特意新增了一个 CacheRepository 来体现提取 Model 层的用意,这样就可以在 Model 层进行不同渠道的分发,既体现了单一职责原则,又很好的提高了代码的可读性。所以看上去是多了一层 Model 层,可实际上作用还是很大的。另外,由于 MVP 是基于接口编程,所以会多了很多接口文件,来定义业务约束,虽然看上去需要新增很多文件/函数,或者理解为额外工作量,但其实这也很好地锻炼架构思维和抽象能力,因为你可以完全放下业务实现来搭建整体框架。
代码
Model
IModel.java
public interface IModel {}
BaseModel.java
public abstract class BaseModel implements IModel {}
View
public interface IView {void showErr(String errMsg);
}
BaseActivity.java
public abstract class BaseActivity<P extends IPresenter> extends AppCompatActivityimplements IView {protected P mPresenter;public BaseActivity() {this.mPresenter = createPresenter();mPresenter.attachView(this);}public abstract P createPresenter();@Overrideprotected void onDestroy() {super.onDestroy();// Activity 销毁时,需要调用 detachView,防止内存泄漏if (mPresenter != null) {mPresenter.detachView();mPresenter.onDestroy();mPresenter = null;}}@Overridepublic void showErr(String errMsg) {Toast.makeText(this, errMsg, Toast.LENGTH_SHORT);}
}
Presenter
IPresenter.java
public interface IPresenter<V extends IView> {void attachView(V view);void detachView();void onDestroy();
}
BasePresenter.java
public abstract class BasePresenter<V extends IView, M extends IModel> implements IPresenter<V> {protected WeakReference<V> mView;protected M mModel;public BasePresenter() {this.mModel = createModel();}protected abstract M createModel();@Overridepublic void attachView(V view) {mView = new WeakReference<>(view);}@Overridepublic void detachView() {if (mView.get() != null) {mView.clear();}}@Overridepublic void onDestroy() {if (mModel != null) {mModel = null;}}
}
上述代码中可以看到,Presenter 中持有 View 引用,想象一种情况,Activity 发起一个网络请求/耗时操作,Presenter 收到需求后就分发去处理需求并等待结果了,但是还没等处理结束,Activity 就执行关闭操作了,此时 Presenter 还持有着 Activity 的强引用,导致 Activity 无法被及时回收掉,这便导致了大名鼎鼎的内存泄漏了;上述代码中通过 WeakReference 持有 View 引用,这样可以有效解决内存泄漏问题,并且在涉及到 Model/View/Presenter 的引用调用的地方,都进行了非空判断,需要规避空指针的风险出现。
Android 中 MVP 的问题
不幸的是,MVP 中 Presenter 的职责就是 MVC 中 Controller 负责的内容,只不过在 Android 中 Controller 在全职作 控制器 的同时,还需要兼职一部分 View 的职责,而 Presenter 就只是全职作 控制器。所以 Presenter 同样存在 Controller 存在的问题,随着业务的增多,Presenter 会变得越拉越 臃肿/复杂,以及 很糟糕的代码可读性。
另外,由于 MVP 模式依赖于接口,所以在新增一个业务需求时,会 爆炸式增长文件和函数,这个也很让人头疼。
其次,由于 View 会持有 Presenter 引用,Presenter 持有 View 和 Model 的引用,如果处理不当,也会存在 空指针 的风险。
最后,Presenter 会持有 View 的引用,这样就埋下了 内存泄漏 的种子,如果处理不好,问题还是蛮大的。所以就有了后来的MVVM。
试吃个小李子
点击按钮,请求 wanandroid 网站的 banner 接口数据,请求成功后更新到UI上显示接口数据
MVP 架构的 Demo 是从 MVC 架构那套代码变更过来的,添加了很多文件,主要部分 涉及上图中展开的这几个文件,仔细看上图蓝框中的内容会发现,新增一个业务 Activity,共需要新增 6 个文件,其中 3 个是接口约束类、3 个是具体实现类,这也体现了上面说的文件/函数暴增的问题。
Model
请求接口
缓存数据
IMainModel.java
public interface IMainModel extends IModel {/*** 请求 banner 数据** @param callback*/void getNetworkBanner(ResponseCallback<List<Banner>> callback);/*** 读取 banner 本地数据** @return*/List<Banner> getLocalBanner();/*** 持久化存储 banner 数据** @param banners*/void saveBanner(List<Banner> banners);/*** 清空本地数据*/void clearLocalData();
}
MainModel.java
public class MainModel extends BaseModel implements IMainModel {@Overridepublic void getNetworkBanner(ResponseCallback<List<Banner>> callback) {// 收到需求,请求接口数据NetworkRepository.getInstance().requestBanners(callback);}@Overridepublic List<Banner> getLocalBanner() {// 收到需求,读取本地数据return CacheRepository.getInstance().getBanners();}@Overridepublic void saveBanner(List<Banner> banners) {// 收到需求,持久化存储 banner 数据CacheRepository.getInstance().saveBanners(banners);}@Overridepublic void clearLocalData() {// 收到需求,清空本地缓存数据CacheRepository.getInstance().clearLocalData();}
}
View
Button1,点击请求接口数据
Button2,获取本读缓存数据
Button3,清空本地缓存数据
TextView,用于回显数据
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"tools:context=".main.MainActivity"><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="getNetworkInfo"android:text="@string/get_network_info" /><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="getLocalInfo"android:text="@string/get_local_info" /><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="clearLocalInfo"android:text="@string/clear_local_info" /><TextViewandroid:id="@+id/tv_banner_info"android:layout_width="wrap_content"android:layout_height="wrap_content" /></LinearLayout>
IMainView.java
public interface IMainView extends IView {/*** 更新 banner 数据** @param banners*/void updateBanner(List<Banner> banners, String from);
}
MainActivity.java
public class MainActivity extends BaseActivity<IMainPresenter> implements IMainView {private TextView mBannerInfoTv;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mBannerInfoTv = (TextView) findViewById(R.id.tv_banner_info);}@Overridepublic IMainPresenter createPresenter() {return new MainPresenter();}/*** 按钮点击事件** @param view*/public void getNetworkInfo(View view) {// 收到点击事件,交给 presenter 进行业务处理if (mPresenter != null) {mPresenter.getNetworkBanner();}}/*** 按钮点击事件** @param view*/public void getLocalInfo(View view) {// 收到点击事件,交给 presenter 进行业务处理if (mPresenter != null) {mPresenter.getLocalBanner();}}/*** 按钮点击事件** @param view*/public void clearLocalInfo(View view) {// 收到点击事件,交给 presenter 进行业务处理if (mPresenter != null) {mPresenter.clearLocalData();}}@Overridepublic void updateBanner(List<Banner> banners, String from) {// 收到更新 ui 事件,更新 uishowBannerInfo(banners, from);}/*** 更新UI** @param banners*/private void showBannerInfo(List<Banner> banners, String from) {StringBuilder sb = new StringBuilder();if (banners.size() > 0) {sb.append("wanandroid 官网\nhttps://www.wanandroid.com\n\n").append("data from ").append(from).append(":\n");for (Banner item : banners) {Log.e("banner", item.toString());sb.append(item.getTitle()).append('\n');}}mBannerInfoTv.setText(sb.toString());}
}
Presenter
业务处理
IMainPresenter.java
public interface IMainPresenter<V extends IView> extends IPresenter<V> {/*** 获取 banner 网络数据*/void getNetworkBanner();/*** 获取 banner 本地数据*/void getLocalBanner();/*** 存储 banner 数据** @param banners*/void saveBanner(List<Banner> banners);/*** 清空本地数据*/void clearLocalData();
}
MainPresenter.java
public class MainPresenter extends BasePresenter<IMainView, IMainModel>implements IMainPresenter<IMainView> {@Overrideprotected IMainModel createModel() {return new MainModel();}@Overridepublic void getNetworkBanner() {if (mModel == null) {return;}// 收到新需求,分发给 model 处理mModel.getNetworkBanner(new ResponseCallback<List<Banner>>() {@Overridepublic void onSuccess(List<Banner> banners) {// 数据缓存saveBanner(banners);// 通知更新UInotifyUpdateBanner(banners, CommonConstant.FROM_NETWORK);}@Overridepublic void onFail(String msg) {IMainView view = mView.get();if (view != null) {view.showErr(msg);}}});}@Overridepublic void getLocalBanner() {if (mModel == null) {return;}// 收到新需求,分发给 model 处理List<Banner> banners = mModel.getLocalBanner();// 通知更新UInotifyUpdateBanner(banners, CommonConstant.FROM_LOCAL);}@Overridepublic void saveBanner(List<Banner> banners) {// 收到新需求,分发给 model 处理if (mModel != null) {mModel.saveBanner(banners);}}@Overridepublic void clearLocalData() {// 收到新需求,分发给 model 处理if (mModel != null) {mModel.clearLocalData();}}/*** 通知更新UI** @param banners*/private void notifyUpdateBanner(List<Banner> banners, String from) {// 获取到数据,通知更新 uiif (mView != null) {IMainView view = mView.get();if (view != null) {view.updateBanner(banners, from);}}}
}
效果展示
附上源码链接
致谢:
感谢 wanandroid 提供的开放API
参考:
一个小例子彻底搞懂 MVP
写在最后:
很荣幸成为一名 Android 程序猿,虽然不是一名合格的猿。一路走来磕磕绊绊,借此感谢帮助过我的人,感谢指点、感恩遇见!