Model-View-Presenter
英文原文
本文主要介绍 Model-View-Presenter (MVP)的原理,以及如何使用Mosby创建基于MVP的应用程序。
- model 是将在视图(用户界面)中显示的数据。
- view 是显示数据(model)并将用户命令(事件)传递到 Presenter 以对该数据执行操作的界面。view 通常对其Presenter有一个引用。
- Presenter 是“中间人”(就像MVC中的controller),并具有view和model的引用。请注意,“Model”一词是误导性的。它应该是检索或操纵模型的业务逻辑。例如:如果你有一个在数据库表中存储User的数据库,并且你的View想要显示一个User列表,那么Presenter会引用数据库中的业务逻辑层(比如DAO)从而查询到一个User列表。
[图片上传失败...(image-ff1b01-1513151286080)]
查询和显示来自数据库的用户列表的具体工作流程:
[图片上传失败...(image-190b62-1513151241674)]
上面显示的工作流程图应该是很容易理解的。不过这里有一些额外的想法:
Presenter
并不是OnClickListener
。View
负责处理用户输入并调用Presenter
的相应方法。为什么不通过将Presenter
变成OnClickListener
从而消除这种"转移"的过程呢?如果这样做,Presenter需要了解有关视图内部的知识。例如,如果一个View有两个按钮,并且这个view在这两个按钮上都把Presenter
注册成OnClickListener
,那么Presenter
如何区分哪个按钮被点击了(在不知道view按钮引用等内部构造的情况下)? Model,View和Presenter应该分离。而且,如果让Presenter
实现OnClickListener
,Presenter就被绑定到了android平台。从理论上说,Presenter和业务逻辑应该能够在桌面程序或其他java程序间共享的普通java代码。就像在步骤1和步骤2中看到的,
View
只做Presenter
告诉View
需要做的那些操作:用户点击了“load user button”(第1步)之后,view不会直接显示加载动画。而是在步骤2由Presenter明确地告诉view去显示加载动画。Model-View-Presenter的这种变体被称为被动视图(Passive View)。view应该尽可能愚蠢。让Presenter以抽象的方式控制view。例如:Presenter调用view.showLoading()
,而不是控制view中特定的东西,如动画。所以,Presenter不应该调用view.startAnimation()
这种方法。通过实现MVP被动视图,处理并发和多线程更容易。就像您在步骤3中看到的那样,数据库查询异步运行,Presenter是一个监听器Listener/观察者Observer,并在数据准备好显示时得到通知。
Android上的MVP
到现在为止还挺好。但是如何在自己的Android应用上应用MVP?第一个问题是,我们应该在哪里应用MVP模式?在Activity上,Fragment上,还是在像RelativeLayout这样的ViewGroup上?让我们来看看Gmail Android平板应用程序:
[图片上传失败...(image-c3a04-1513151241674)]
在我们看来,在上图所示的屏幕上有四个独立的可使用MVP的地方。“可以使用MVP的地方”是指屏幕上显示的、在逻辑上属于一个整体的UI元素。因此这些地方也可以称为是可以运用MVP的一个单独的UI单元。
[图片上传失败...(image-b543c2-1513151241674)]
这样看起来MVP似乎适合运用到Activity,特别是Fragment上。通常一个Fragment负责显示一个像ListView一样的内容。例如上图中被使用MailProvider
获取Mails
列表的InboxPresenter
控制的InboxView
。但是,MVP不限于Fragment 和 Activity。你也可以在ViewGroups
上应用这个设计模式,如上图所示的SearchView
。在许多app中都在Fragment上使用MVP。然而,这都取决于你想要把MVP运用到什么地方。只要确保view是独立的,以便一个Presenter可以控制这个view,而不会与另一个Presenter发生冲突。
我们为什么要实现MVP?
思考一下,如果不使用MVP,你将如何在Fragment中实现收件箱view,来显示从本地sql数据库和IMAP邮件服务器两个数据源得到的邮件列表。你的Fragment代码会是什么样子?或许,你将启动两个AsyncTasks
并且必须实现一个“等待机制”(等到两个任务都完成),然后将两个任务得到的邮件列表合并成一个邮件列表。你还需要注意,在加载时显示加载动画(ProgressBar),之后用ListView替换它。你会把所有的代码放入Fragment吗?如果加载时出现了错误怎么办?如果屏幕方向改变了呢?谁负责取消AsyncTasks
?这一系列的问题都可以用MVP来解决。让我们向1000+行、大杂烩似的Activity和Fragment代码说再见吧。
但是在我们深入了解如何在Android上实现MVP之前,我们必须澄清一下,Activity和Fragment到底是View
还是Presenter
。Activity和Fragment似乎既是View又是Presenter,因为他们都有onCreate()
和onDestroy()
这种生命周期回调,也有像从一个UI控件切换到另一个UI控件(例如,加载时显示一个ProgressBar,然后显示一个带有数据的ListView)的View职责。你可以说这些听起来Activity和Fragment更像是一个Controller。然而,我们得出的结论是Activity和Fragment应该被视为(愚蠢的)View,而不是Presenter。后面你会看到原因。
有了这个说法,我们想要介绍Mosby
,这是一个在android上创建基于MVP的应用程序的库。
Mosby
你可能已经发现,如果你试图去解释MVP是MVC(Model-View-Controller)的变种或改进,那么就很难理解什么是Presenter。尤其是iOS开发人员,他们很难理解Controller和Presenter的区别, because they “grew up” with the fixed idea and definition of an iOS alike UIViewController
。在我们来看,MVP并不是MVC的变种或改进,因为这意味着Presenter取代了Controller。我们认为,MVP包装了MVC。看看你使用MVC开发的app。通常你有你的View和Controller(即Android中的Fragment或iOS的UIViewController)处理点击事件,绑定数据和观察ListView(或在iOS上为UITableView实现一个UITableViewDelegate)等等。现在退一步,想象一下,controller就是view的一部分,而不是直接连接到你的model(业务逻辑)。而Presenter位于controller 和model的中间,如下所示:
[图片上传失败...(image-821785-1513151241674)]
让我们来看一个具体的例子:示例程序显示从数据库中查询的用户列表。当用户点击“加载按钮”时开始执行。查询数据库(异步)时ProgressBar显示,然后 ListView显示出查询结果。
我们认为Presenter不会取代Controller。而是Presenter协调并监督Presenter所属的View。Controller是处理点击事件并调用相应的Presenter方法的组件。Controller是负责控制动画的组件,如隐藏ProgressBar并显示ListView。Controller监听ListView上的滚动事件,即在滚动ListView时进行一些item动画或显示隐藏toolbar。因此,所有与UI相关的东西仍然受Controller而不是Presenter控制(即Presenter不应该是一个OnClickListener)。Presenter负责协调view层(由UI控件和Controller组成)的整体状态。因此,Presenter的工作是告诉view层现在应该显示加载动画,然后在数据准备好后,显示ListView。
MvpView和MvpPresenter
所有view的基类是MvpView
。本质上它只是一个空的interface。该接口为Presenter提供了一个公共API来调用View相关的方法。Presenter的基类是MvpPresenter
:
public interface MvpView { }
public interface MvpPresenter {
public void attachView(V view);
public void detachView(boolean retainInstance);
}
这一理念是MvpView
(即Fragment or Activity)会去关联和取消关联一个MvpPresenter
。这样一来Mosby获取到Activity和Fragment的生命周期(更多内容可以查看下面“委托”部分的内容)。因此,初始化和清理东西(如取消异步运行任务)的操作应该在presenter.attachView()
和presenter.detachView()
中执行。
Mosby提供了Presenter的另一种实现MvpBasePresenter
,它使用WeakReference
来保存对view(Fragment or Activity)的引用,以避免内存泄漏。因此,当你的Presenter想要调用view的方法时,您必须通过调用isViewAttached()
来检查这个view是否被关联到你的Presenter,并通过使用
getView()
来或者view的引用。
另外,你可以为你的MvpView
使用实现了空对象模式的MvpNullObjectBasePresenter
。所以无论什么时候MvpNullObjectBasePresenter.onDetach()
被调用,view都不会被设置为null(像MvpBasePresenter这样),而是通过使用反射来动态创建一个空view,并将其作为view关联到Presenter中。这就避免了在方法调用时检查view != null
。
MvpActivity和MvpFragment
如前所述,我们将Activity 和 Fragment当作View。如果你只是想要一个由Presenter控制的Activity 或 Fragment,你可以在你的程序中使用实现了MvpView
的MvpActivity
和MvpFragment
用作基类。为了确保类型安全,建议这样使用:MvpActivity
和MvpFragment
加载内容错误(LCE)
通常你会发现自己在应用程序中一遍又一遍地写同样的东西:在后台加载数据,在加载时显示加载视图(即ProgressBar),显示加载的数据或加载错误时显示错误消息。由于SwipeRefreshLayout
成为Android的支持库的一部分,现在支持下拉刷新是很容易的。为了不重复实施这个工作流程Mosby提供了MvpLceView
:
/**
* @param The type of the data displayed in this view
*/
public interface MvpLceView extends MvpView {
/**
* Display a loading view while loading data in background.
* The loading view must have the id = R.id.loadingView
*
* @param pullToRefresh true, if pull-to-refresh has been invoked loading.
*/
public void showLoading(boolean pullToRefresh);
/**
* Show the content view.
*
* The content view must have the id = R.id.contentView
*/
public void showContent();
/**
* Show the error view.
* The error view must be a TextView with the id = R.id.errorView
*
* @param e The Throwable that has caused this error
* @param pullToRefresh true, if the exception was thrown during pull-to-refresh, otherwise
* false.
*/
public void showError(Throwable e, boolean pullToRefresh);
/**
* The data that should be displayed with {@link #showContent()}
*/
public void setData(M data);
}
上面说的那种view,你可以使用MvpLceActivity implements MvpLceView
和MvpLceFragment implements MvpLceView
来实现。这两个都假设XML布局中包含了含有R.id.loadingView
,R.id.contentView
和R.id.errorView
的view。
示例
在下面的示例中(托管在Github上),我们通过使用CountriesAsyncLoader
加载Country
列表并在Fragment的RecyclerView中显示。
我们首先定义视图界面CountriesView
:
public interface CountriesView extends MvpLceView> {
}
为什么我需要为View定义接口?
由于它是一个接口,你可以改变view的实现。我们可以简单的将代码从继承Activity的实现中拷贝到继承Fragment的实现中。
模块化:您可以将整个业务逻辑,Presenter和View Interface移动到独立的库中。然后,把这个包含了Presenter的库应用到各种app中。
您可以轻松编写单元测试,因为您可以通过实现view interface来模拟视图。还有一个更简单的方法就是在presenter中引入java接口并模拟presenter对象来编写单元测试。
为视图定义一个接口的另一个好处是,你不需要直接从Presenter中调用activity / fragment的方法。因为在实现Presenter的时候,你在IDE的自动完成提示中只能看到view interface的那些方法。根据我们的个人经验,我们可以说,这是非常有用的,特别是如果你在一个团队中工作。
请注意,我们也可以使用MvpLceView
,而不是定义一个(空的,因为继承方法)接口>
CountriesView
。但是有一个专用的接口CountriesView可以提高代码的可读性,而且我们可以在将来更灵活地定义更多的与View有关的方法。
接下来我们用所需的id来定义我们view的xml布局文件:
CountriesPresenter
控制CountriesView
并启动CountriesAsyncLoader
:
public class CountriesPresenter extends MvpBasePresenter {
@Override
public void loadCountries(final boolean pullToRefresh) {
getView().showLoading(pullToRefresh);
CountriesAsyncLoader countriesLoader = new CountriesAsyncLoader(
new CountriesAsyncLoader.CountriesLoaderListener() {
@Override public void onSuccess(List countries) {
if (isViewAttached()) {
getView().setData(countries);
getView().showContent();
}
}
@Override public void onError(Exception e) {
if (isViewAttached()) {
getView().showError(e, pullToRefresh);
}
}
});
countriesLoader.execute();
}
}
实现CountriesView
的CountriesFragment
如下:
public class CountriesFragment
extends MvpLceFragment, CountriesView, CountriesPresenter>
implements CountriesView, SwipeRefreshLayout.OnRefreshListener {
@Bind(R.id.recyclerView) RecyclerView recyclerView;
CountriesAdapter adapter;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.countries_list, container, false);
}
@Override public void onViewCreated(View view, @Nullable Bundle savedInstance) {
super.onViewCreated(view, savedInstance);
// Setup contentView == SwipeRefreshView
contentView.setOnRefreshListener(this);
// Setup recycler view
adapter = new CountriesAdapter(getActivity());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
loadData(false);
}
public void loadData(boolean pullToRefresh) {
presenter.loadCountries(pullToRefresh);
}
@Override protected CountriesPresenter createPresenter() {
return new SimpleCountriesPresenter();
}
@Override public void setData(List data) {
adapter.setCountries(data);
adapter.notifyDataSetChanged();
}
@Override public void onRefresh() {
loadData(true);
}
}
没有太多的代码要写,对吧?这是因为基类MvpLceFragment
已经帮我们实现了从加载视图切换到内容视图或者错误视图。乍一看你可能会被MvpLceFragment
那一串泛型参数列表吓到。让我解释一下:第一个泛型参数是content view 的类型(从android.view.View延伸的东西)。第二个是fragment要显示的Model。第三个是View接口,最后一个是Presenter的类型。总结:MvpLceFragment
ViewGroup
如果你想避免使用Fragment,你可以做到这一点。Mosby为ViewGroups
提供了与Activities and Fragments相同的MVP脚手架。API与Activity和Fragment的相同。一些默认的实现像MvpFrameLayout
,MvpLinearLayout
和MvpRelativeLayout
已经提供使用了。
Delegation委托
您可能想知道,Mosby如果不使用代码复制(复制和粘贴相同的代码),是如何为所有类型的view(Activity,Fragment和ViewGroup)提供相同的API的。答案是delegation委托。委托的方法已被命名为与Activity或Fragments生命周期的方法名称(受appcompat支持库中最新的AppCompatDelegate的启发)相匹配的名称,以更好地理解应从哪个Activity或Fragment生命周期方法调用哪个委托方法:
MvpDelegateCallback
:是每个Mosby中的MvpView
都必须实现的接口。基本上它只是提供了一些MVP相关的方法像createPresenter()
等。这个方法在内部被ActivityMvpDelegate
或FragmentMvpDelegate
调用。ActivityMvpDelegate
:这是一个接口。通常你使用ActivityMvpDelegateImpl
这个默认的实现。要想在你自己的Activity中引入Mosby MVP,你需要做的是,从Activity的onCreate()
,onPause()
,onDestroy()
等生命周期方法中调用相应的委托方法,并实现MvpDelegateCallback
:
public abstract class MyActivity extends Activity implements MvpDelegateCallback<> {
protected ActivityMvpDelegate mvpDelegate = new ActivityMvpDelegateImpl(this);
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mvpDelegate.onCreate(savedInstanceState);
}
@Override protected void onDestroy() {
super.onDestroy();
mvpDelegate.onDestroy();
}
@Override protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mvpDelegate.onSaveInstanceState(outState);
}
... // other lifecycle methods
}
FragmentMvpDelegate
:同ActivityMvpDelegate
一样。要在你的Fragment中引入Mosby MVP的支持,您所要做的就和上面在Activity中引入一样:创建一个FragmentMvpDelegate
,并从Fragment的生命周期方法中调用相应的委托方法,你的Fragment同样也必须实现MvpDelegateCallback
。通常你可以使用默认的委托实现FragmentMvpDelegateImpl
ViewGroupMvpDelegate
:这个委托是给ViewGroup
用的。在你的ViewGroup
中引入Mosby MVP,生命周期方法要比Fragment的更简单:onAttachedToWindow()
和onDetachedFromWindow()
。默认的实现是ViewGroupMvpDelegateImpl
。
委托的另一个优点是可以将Mosby整合到其他任何一个第三方库或框架中。只需实现MvpDelegateCallback
并实例化一个委托,并在生命周期事件中调用相应的委托方法。
演示模型
在理想世界中,我们通过最佳的方式得到在我们的GUI(View)中显示的数据。很多时候,我们通过公共API检索后端数据,这些公共API无法为了适应UI的需求而更改。实际上,后端根据你的用户界面提供一个API并不是一个好主意,因为如果你改变你的用户界面,你也可能需要改变后端。因此,您必须将model转换,从而使你的GUI可以轻松的显示。一个典型的例子是从一个REST json API中加载一个Items列表,比方说一个用户列表,并将它们显示在一个ListView中。使用MVP,在真实环境中这个工作是这样的:
[图片上传失败...(image-94dd30-1513151241674)]
这里没有新东西。List
被加载并且GUI 通过使用 UserAdapter
在ListView
中显示用户。我敢肯定,你之前已经千万次的使用了ListView
和Adapter
,但你可曾想过背后的想法Adapter
?Adapter通过android UI控件使你的model可以显示出来。这就是适配器设计模式adapter design pattern。如果我们想要支持手机和平板电脑,还都以不同的方式显示item呢?我们是实现两个适配器:PhoneUserAdapter
和TabletUserAdapter
,然后在运行时选择合适的适配器么。
如果那样做,就真是“理想情况”了。如果我们必须对用户列表进行排序或者显示一些必须通过复杂(和CPU密集型)方式进行计算的事情呢?我们不能在UserAdapter
中那样做,因为在主UI线程上做那些繁重的工作会导致listview滚动性能问题。因此,我们放到一个单独的线程中去做。随之而来的有两个问题:第一个是我们如何转换数据?我们拿我们的用户类,并添加一些额外的属性么?我们是否覆盖用户类的值?
public class User {
String firstname;
String lastname;
}
我们假设我们UserView
想要显示全名,并计算一个排名使列表排序:
public class User {
String firstname;
String lastname;
int ranking;
public String getFullname(){
return firstname +" "+lastname;
}
}
虽然引入方法getFullname()
是可以的,但添加ranking
字段可能会导致问题,想象一下我们从后端检索得到的User
可能并没有ranking
。所以首先,如果你看看你的json api提要,并将它与我们的User类进行比较,最后但不是最不重要的ranking 将设为默认值零,因为我们还没有计算出排名。如果我们使用了一个对象而不是一个整数,那么默认值就是null,并且很可能会遇到NullPointerException。
解决方案是引入一个 Presentation Model。这个模型只是为我们的GUI优化的一个类:
public class UserPresentationModel {
String fullname;
int ranking;
public UserPresentationModel(String fullname, int ranking) { ... }
}
通过这样做,我们确定ranking
始终被设置为一个具体值,并且在滚动ListView时不会计算fullname(PresentationModel在独立线程中实例化)。UserView现在显示List
而不是List
。
第二个问题是:在哪里做异步转换?View, Model 还是 Presenter? 很明显,View进行这种转换操作,因为View知道如何在屏幕上显示事物。
[图片上传失败...(image-4a5c8d-1513151241674)]
PresentationModelTransformer
是接受List
并将其“转换”到List
的组件(适配器模式,所以我们有两个adapter:一个转换为表示模型,另一个是在ListView中显示它们的UserAdapter)。在view中整合PresentationModelTransformer
的优势在于,view知道如何显示内容,并且可以在内部轻松切换 手机和平板电脑优化了的演示模型(可能平板电脑的用户界面跟手机比还有其他需求)。但是,最大的缺点是现在view必须控制异步线程和视图状态(在进行转换时显示ProgressBar?!?),这显然是Presenter的工作。因此,让转换成为view的一部分并不是一个好主意。在Presenter中包括转换是将要做的:
[图片上传失败...(image-bad7b2-1513151241674)]
正如我们前面已经讨论的那样,Presenter
负责协调View,因此Presenter告诉view在UserPresentationModel
转换完成后显示ListView 。此外,Presenter可以控制所有异步线程(转换的异步线程),并在必要时取消它们。顺便说一下:使用RxJava
,你可以使用类似map()
或者flatMap()
操作符进行转换。如果我们想要支持手机和平板电脑,我们可以定义两个实现了不同PresentationModelTransformer
的Presenter PhoneUserPresenter
和TabletUserPresenter
。在Mosby,View创建Presenter。由于在运行时View知道是手机还是平板电脑,因此可以在运行时选择不同的Presenter实例化(PhoneUserPresenter或TabletUserPresenter)。或者,你可以为手机和平板电脑使用同一个UserPresenter
,仅通过使用依赖注入替换PresentationModelTransformer
的实现。