MVP是从MVC衍生出来的,因此,先说一下MVC,再到MVP。java
MVC分为三个部分:android
视图(View):用户界面。git
控制器(Controller):业务逻辑,用于控制应用程序的流程。github
模型(Model):数据处理。即网络请求、数据库等一些对数据的操做处理。数据库
它们之间的交互一般以下图:编程
那么在咱们android里,如何划分这几层?在这里,参考部分博文,以及根据我我的的理解,列出下面两种可能的状况:bash
(一)View:xml。Controller:Activity/Fragment。Model:数据处理。网络
若是是这样划分的话。实际运用中,每每会出现Activity/Fragment虽然划分为Controller层,但看起来又像View层。架构
为何?由于若是只用xml做为View层,对界面的控制能力实在太弱了,没法动态更新UI。因此,Activity/Fragment也要担当起一部分View层的责任,负责动态更新视图。这就致使Activity/Fragment既有更新View控件的代码,又有对源自Model的数据进行进一步逻辑处理的代码,随着迭代开发,Activity/Fragment会显得愈来愈臃肿,分分钟几千行代码,给后期维护带来极大的困扰。若是你接手这样的代码,内心恐怕会不停赞叹前人真牛逼!mvc
不少博客,描述MVC时,都是这样划分MVC的,并指出:“View层和Model层是相互可知的,这意味着两层之间存在耦合”。
我我的以为这话有谬误。View层和Model层必定是互相可知的?MVC的交互图,V与M的箭头,只表明它们能够互相给对方发送消息。但并不意味着它们之间就必定互相可知。
若是View层和Model层是相互可知的,这也就意味着它们互相持有对方的引用,是经过对方的引用来给彼此发送消息。这确实意味着两层之间存在耦合。下面是这种状况下,View和Model交互的代码。
class XActivity {
public void attachModel() {
VersionModel model = new VersionModel(this);
model.checkUpdate();
}
public void onResult(String result) {
System.out.println(this.getClass().getSimpleName() + "收到了回应:" + result);
}
}
class VersionModel {
private XActivity mActivity;
//注意:activity的引用经过构造方法传递了进来
public VersionModel(XActivity activity) {
mActivity = activity;
}
public void checkUpdate() {
System.out.println("检查更新!");
//用activity的引用传递更新信息。这样,该方法就会绑定死了这个activity。
mActivity.onResult("暂无更新!");
}
}
public class Couple {
public static void main(String[] args) {
//这个VersionModel只能提供给XActivity这个界面使用。
XActivity xActivity = new XActivity();
xActivity.attachModel();
}
}/* Output:
检查更新!
XActivity收到了回应:暂无更新!
*/
复制代码
但这样的代码,实在是太糟糕了。由于Model层的代码,复用性是颇有必要的。好比,这个检查版本更新的Model,也许除了闪屏页须要,你的设置界面也须要有这个功能。像上面这样写,那你的Model和View绑定死了。因此,更恰当的作法,是下面这样。Model的数据处理结果,经过接口回调给View,保证Model的复用性。
class XActivity {
public void attachModel() {
//跟前面不一样,再也不是直接传递自身引用给Model,而是传一个回调接口,经过回调接口,获取数据处理结果
VersionModel model = new VersionModel(new Callback() {
@Override
public void onResult(String result) {
System.out.println(XActivity.this.getClass().getSimpleName() + "收到了回应:" + result);
}
});
model.checkUpdate();
}
}
class YActivity {
public void attachModel() {
//跟前面不一样,再也不是直接传递自身引用给Model,而是传一个回调接口,经过回调接口,获取数据处理结果
VersionModel model = new VersionModel(new Callback() {
@Override
public void onResult(String result) {
System.out.println(YActivity.this.getClass().getSimpleName() + "收到了回应:" + result);
}
});
model.checkUpdate();
}
}
interface Callback {
void onResult(String result);
}
class VersionModel {
private Callback callback;
//再也不是直接接受某个Activity实例作参数,这样就不会再与某个Activity过于耦合,提升了复用性
public VersionModel(Callback callback) {
this.callback = callback;
}
public void checkUpdate() {
System.out.println("检查更新!");
//用的是回调接口,传递更新信息。该方法,不会再是绑定死某个activity。
callback.onResult("暂无更新!");
}
}
public class Decouple {
public static void main(String[] args) {
//这个VersionModel能够随意提供给N个界面使用。
XActivity xActivity = new XActivity();
xActivity.attachModel();
YActivity yActivity = new YActivity();
yActivity.attachModel();
}
}/* Output:
检查更新!
XActivity收到了回应:暂无更新!
检查更新!
YActivity收到了回应:暂无更新!
*/
复制代码
(二)View:"xml+Activity/Fragment"视为View层,仅仅负责控件更新。Controller:将Activity/Fragment中的逻辑控制,包括对源自Model层的数据的进一步处理的操做,抽取出来,到相似XxxController这样命名的类里,做为Controller。做为Controller。Model:数据处理。
这是我本身瞎琢磨的一种划分方法。
若是这样划分,能够极大地减轻了View层的负担。可是,如今业务逻辑处理都被抽调到了Controller。而当Controller须要根据Model的数据处理结果,来决定View下一步作什么的时候,只能经过View去得到(由于View层和Model层能够互相交互。而Controller只能单方向跟Model发消息)。而View自身已经不负责业务逻辑处理了,还得夹在它们中间,简直画蛇添足。
MVP的写法,其实就是基于这种划分的。但使用MVP来实现,不会有这种尴尬。
若是MVC改一改:Controller更名为Presenter,View和Model再也不容许交互,Controller(Presenter)和Model之间的单向通信,改成双向。那么这种作法,就是所谓的MVP了。
Google开源的MVP架构示例项目:android-architecture,也是基于这种划分:
View:xml+Activity/Fragment。
Presenter:根据业务逻辑,对Model得到的结果进一步处理,最后决定View何时更新UI。
Model:数据处理。
以下图:
android-architecture这个项目有不少分支,本文主要参考的是todo-mvp、todo-mvp-rxjava、todo-mvp-dagger三个分支。这几个分支对MVP的划分是一致的,只是实现细节上的差异。想学习rxjava2和dagger2的,推荐翻阅这两个分支。
MVP的运做过程,大体能够这么理解:View给Presenter指派任务,而后Presenter调控一个或多个Model,给它们划分各类小任务,配合完成任务。最后,Model经过回调接口,告诉Presenter任务结果,而后Presenter根据任务的完成状况,通知View更新UI。
以前用于mvc讨论的检查更新功能,若是用MVP写。关键的代码以下:
public interface SplashContract {
interface View{
void showUpdateDialog(VersionBean versionBean);
void jumpToMain();
}
interface Presenter{
void attachView(SplashContract.View view);
void detachView();
void checkUpdate();
}
}
public interface VersionCallback{
void onUpdate(VersionBean versionBean);
void onNoUpdate();
}
public class VersionBean{
private int versionCode;
private String versionName;
private String updateUrl;
public VersionBean(int versionCode,String versionName,String updateUrl){
this.versionCode = versionCode;
this.versionName = versionName;
this.updateUrl = updateUrl;
}
//省略get/set方法
......
}
public class SplashActivity extends AppCompatActivity implements SplashContract.View{
private SplashContract.Presenter mPresenter;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
attachPresenter();
checkUpdate();
}
public void attachPresenter(){
//注意,使用的是Application的Context
VersionModel versionModel = new VersionModel(getApplicationContext());
mPresenter = new SplashPresenter(versionModel);
mPresenter.attachView(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter.detachView();
}
public void checkUpdate(){
mPresenter.checkUpdate();
}
@Override
public void showUpdateDialog(VersionBean versionBean){
//弹更新提示dialog
}
@Override
public void jumpToMain(){
//跳转到MainActivity
}
}
public class SplashPresenter implements SplashContract.Presenter{
private VersionModel mVersionModel;
private SplashContract.View mView;
public SplashPresenter(VersionModel versionModel){
mVersionModel = versionModel;
}
@Override
public void attachView(SplashContract.View view){
mView = view;
}
public void onDetachView() {
//能够在此取消全部的异步订阅
}
@Override
public void checkUpdate(){
mVersionModel.checkUpdate(new VersionCallback(){
@Override
public void onUpdate(VersionBean versionBean){
//异步任务回来后,先判断View是否处于正确的状态
if(mView != null){
mView.showUpdateDialog(versionBean);
}
}
@Override
public void onNoUpdate(){
//异步任务回来后,先判断View是否处于正确的状态
if(mView != null){
mView.jumpToMain();
}
}
});
}
}
public class VersionModel{
private Context mContext;
public VersionModel(Context context){
mContext = context;
}
public void checkUpdate(VersionCallback callback){
//经过网络获取更新信息
VersionBean versionBean = fromServer();
if(BuildConfig.VERSION_CODE < versionBean.getVersionCode()){
callback.onUpdate(versionBean);
}else{
callback.onNoUpdate();
}
}
}
复制代码
区区一个检测更新而已,这代码量会不会有点多了?
基于todo-mvp的mvp写法,我的概括的注意事项以下:
你能够理解为,View是很懒很懒的,不肯意动脑子,几乎全部的逻辑处理,哪怕只是简单的“1+1=?”的问题,都推给Presenter。我以为,除了实现视图和逻辑的分离,还有一部分缘由是:若是是Presenter太臃肿,你彻底能够根据功能不一样,轻易拆分红两个甚至更多的Presenter。可是若是Activity/Fragment(View)太臃肿的话,可能就很差拆分了。
因此,View只负责初始化自身,根据须要,给Presenter指派任务,而后进入“瞌睡”状态。只有两种状况才会被从新唤醒:
1)当接受来自外界的刺激时,好比:点击事件;
2)当Presenter有处理返回:嗨,孙子,醒醒,起来倒茶了(更新UI)。
下面是接口写法的示例。
public interface XxxContract {
interface View{
}
interface Presenter{
}
}
复制代码
Activity/Fragment实现XxxContract.View接口,Presenter实现XxxContract .Presenter接口。二者在使用对方的实例时,一般都会向上转型为对应的接口,也就是说,暴露给对方的方法,都会写在接口里面。
其实,纵观谷歌的demo,View和Presenter之间的耦合度一般都是很高的,Presenter是为对应的View量身定制的,复用的可能性不大,一般也没有这个必要。
而这个接口,如其名,更像是一个契约接口。里面两个关键的子接口,在这种状况下,彻底失去了接口存在的最大意义:"多继承"。
“肯定接口是理想选择,于是应该老是选择接口而不是具体的类。”这实际上是一种引诱。固然,对于建立类,几乎在任什么时候刻,均可以替代为建立一个接口和一个工厂。许多人都掉进了这种陷阱,只要有可能就去建立接口和工厂。这种逻辑看起来好像是由于须要使用不一样的具体实现,所以老是应该添加这种抽象性。这实际上已经变成了一种草率的设计优化。任何抽象性都应该是应真正的需求而产生的。若是没有足够的说服力,只是为了以防万一添加新接口,并由此带来了额外的复杂性。那么应该质疑一下这样的设计了。恰当的原则,应该是优先选择类而不是接口。接口是一种重要工具,但它们容易被滥用。” ——摘自《Java编程思想》第9章 接口 第188-189页
尤为是MVP用得多了,这个契约接口,总让人感到别扭。好比,想在Presenter添加一个方法,除了在Presenter里写它一次,还得在契约接口里面声明一次。它给我带来的麻烦,彷佛多于给个人便利。
这个Contract接口有没有存在的必要,真的有待商榷。
因此,我的更倾向于:去除Contract接口、Presenter接口,只保留View接口。Activity/Fragment则直接使用Presenter的实例时,再也不将其向上转型为接口。在调用Presenter的attachView方法的时候,将Activigty/Fragment的实例向上转型为接口,避免Presenter滥用该实例引用。
以上仅是我的观点,也许鄙人目光短浅,有所谬误。欢迎指正。
由于Presenter的生命周期,一般与Activity/Fragment是不相同的。因此Presenter在执行异步操做后,在结束的时候,都要判断View是否还处于正确的状态。这样,就能有效得避免了在异步任务完成时,Activity/Fragment却已经被销毁而致使的空指针等问题。固然,同步任务一般不须要这种判断。
有趣的是,todo-mvp、todo-mvp-dagger、todo-mvp-rxjava这三个分支对上述操做的作法都不一样,但有殊途同归之妙。下面给出它们作法的关键代码。
todo-mvp
public class TasksFragment extends Fragment implements TasksContract.View {
/**
* Return true if the fragment is currently added to its activity.
*/
@Override
public boolean isActive() {
return isAdded();
}
}
public class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//异步任务
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
//异步任务结束后,再调View以前,必须先判断
//不过这种作法,其实只适合实现View接口的是Fragment的时候
if (!mTasksView.isActive()) {
return;
}
mTasksView.setLoadingIndicator(false);
}
});
}
}
复制代码
todo-mvp-dagger
final class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//异步任务
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
//异步任务结束后,再调View以前,必须先判断
if (mTasksView == null) {
return;
}
mTasksView.setLoadingIndicator(false);
}
});
}
//在View解除绑定时,会被调用(须要你本身在onDestroy/onDestroyView里面手动调用)
@Override
public void dropView() {
mTasksView = null;
}
}
复制代码
todo-mvp-rxjava
public class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//异步任务
Disposable disposable = mTasksRepository
.getTasks()
.subscribe(tasks -> mTasksView.setLoadingIndicator(false));
//将能取消异步任务的disposable添加进CompositeDisposable
mCompositeDisposable.add(disposable);
}
//在View解除绑定时,会被调用(须要你本身在onDestroy/onDestroyView里面手动调用)
// 取消mCompositeDisposable里面添加的全部RxJava的异步任务。
@Override
public void unsubscribe() {
mCompositeDisposable.clear();
}
}
复制代码
就像我上面讲MVC时举的例子,Model不该该持有Presenter的引用,它不该该知道会是哪一个Presenter来调用它。它的数据处理结果,都应该经过回调接口来交给调用它的Presenter。若是,你以为写这些回调接口很烦,那么你能够考虑学习一波RxJava了。
反面示例:
//不该该将Presenter的引用传给Model
public MainModel(MainContract.Presenter mainPresenter) {
this.mainPresenter= mainPresenter;
}
复制代码
这是个很典型的反面例子...我也曾经这么写过Model的代码,直到有一天,我须要在其余地方复用某个Model的时候...笑哭.jpg。
缘由以下:
1)避免Context在子线程使用时,因为Activity忽然被销毁,致使的空指针。
2)避免本身偷懒,在Presenter和Model执行更新UI的操做。实际上,这也是很不该该的操做。我在不少项目里面,常常看到这样的错误操做:在Presenter里更新UI、把Adapter的代码放进Presenter里面等等。MVP的划分,不只仅是为了代码的解耦、复用,也有很大一部分缘由是由于Android类不能直接在JVM上运行,会影响咱们写单元测试。而尽可能隔离、减小使用Android类的Presenter、Model层,会更便于咱们写相关单元测试。
3)Application的Context足够应付Presenter和Model里面的需求了。看下表,Context的应用场景:
*数字1:启动Activity在这些类中是能够的,可是须要建立一个新的task。通常状况不推荐。
*数字2:在这些类中去layout inflate是合法的,可是会使用系统默认的主题样式,若是你自定义了某些样式可能不会被使用。
*数字3:在receiver为null时容许,在4.2或以上的版本中,用于获取黏性广播的当前值。(能够无视) ContentProvider、BroadcastReceiver之因此在上述表格中,是由于在其内部方法中都有一个context用于使用。
你能够看见,Activity的Context能作的,Application的Context基本都能作。若是你非要在Presenter、Model里面使用Activity的Context,我想你更应该考虑是否是该把这段代码放到Activity/Fragment里面。
所谓的依赖注入,就是A类里面须要使用到B类,而B类的实例,在一开始,就先在外面建立好,而后经过A类的构造方法传递进来的。先前的MVP示例里的这段代码,就完成了一个简单的依赖注入。
VersionModel versionModel = new VersionModel(getApplicationContext());
mPresenter = new SplashPresenter(versionModel);
复制代码
如今流行的Dagger2这个依赖注入框架只是能极大地简化了你本身去手动new这些实例,甚至建立这些实例的单例,再注入的过程。
全局的Context,若是要在Presenter、Model里面使用,也要经过构造方法,将Context传递进来。很大的一部分缘由也是:方便写单元测试代码。
由于单元测试里面有一个mock的概念。依赖注入,能方便你mock相应的类。若是对单元测试有兴趣的话,到时能够参考个人相关测试文章。
代码示例: todo-mvp里的某个Presenter构造方法的部分代码
public TasksPresenter(@NonNull TasksRepository tasksRepository) {
/**
* 至关于mTasksRepository = tasksRepository,并检测tasksRepository是否为null。
* 若是为空,抛空指针异常。
*/
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
}
复制代码
注:checkNotNull是谷歌的guava里面的方法。虽然谷歌demo的各个分支,都有用到guava库,一个工具类库。但实际项目中并不推荐适用它,由于android里64k方法的限制。这个库它有1w+方法...若是你喜欢用相关方法,建议把相关类复制出来便可。
前面提到,Model是“数据处理”,Presenter是“根据业务逻辑,对Model得到的结果进一步处理”。不少人也许会对Presenter、Model的划分产生疑问:一样都会进行数据处理,怎么划分才更恰当?
其实MVP三层的关系,从另外一个角度看的话,是层层递进的关系,以下图。
而前面咱们已经知道Model的复用性是很重要。假设,若是有多个Presenter都对Model中得到的数据,进行一样的处理,那么咱们就应该考虑,把这一段处理代码,“下沉”到Model层里。若是Model里的一个方法,有一段代码只针对某个Presenter作了特殊处理,那么咱们就应该考虑,把这段处理代码,“上浮”到那个特定的Presenter里。
相比MVC的各类尴尬,MVP之间的分工合做明显更加合理,便于维护、测试。个中好处,只能你理解和熟练使用MVP后,才能深有感触。
建议认真翻阅谷歌的官方架构demo:android-architecture
我我的也提供了一个用MVP写的讯飞语音交互demo:TalkDemo
若是你对dagger、rxjava熟悉,也能够参阅个人另外一个基于mvp+dagger2(dagger.android)+rxjava2+retrofit2的架构demo:ArchitectureDemo
以上内容,均基于我的的理解和谷歌的架构demo总结的。
若有谬误,请及时提醒,不胜感激!