这篇文章是我学习MVP模式时翻译的,原文是Konstantin Mikheev所写,传送门。
因英语水平有限,翻译的很生硬,基本靠Google,请见谅。以下是译文。
这篇文章我会通过一个最简单的例子去一步步介绍MVP模式在Android中的最佳实践。同时我也会介绍一个使MVP模式在Android开发中变简单的library。
简单?怎么才能从中获益呢?
什么是MVP
View层是用来显示数据和相应数据操作的。在Android中,它可能是Activity,Fragment,View或者Dialog。
Model层是数据访问层,例如数据库API或者远程服务器访问API。
Presenter层提供View层和Model层的数据进行联系。Presenter层也可以控制后台Task。
在Android中,MVP模式可以把后台线程从Activitys/Views/Fragments中脱离出来,使它们在大部分生命周期事件中更加独立。这样的应用变得更加简单,整个程序的稳定性提升了10倍不止,应用代码变得更加少,代码可维护性变得更加友善,开发者的生命变得更加开心。
在Android中,为什么是MVP
原因1:保持简单傻瓜
如果你还没读过这篇文章,请读一遍:The Kiss Principle
·大多数Android程序仅仅使用了View-Model模式。·程序员需要参与View的复杂性,而不是解决业务。
在应用中你仅仅使用Model-View,最后会落得“一切连接这一切”的状态。
如果这个示例图看起来不是那么复杂,那么想想每个View可能随时消失和随时出现。别忘了保存和恢复view的状态。为临时View attache 几个后台任务,蛋糕准备好了!
另一种“一切连接着一切”就是上帝对象。
上地对象太过于复杂;代码块不能被重复利用,测试或方便的debug和重构。
使用MVP模式
·复杂的任务被分解成简单的任务,并且容易解决。·更小的对象,更少的bug,更简单debug。·可测试。
原因2:后台任务
无论何时,你写Activity,Fragment或者自定义View,你可以把所有方法与后台任务的外部或者静态类联系起来。这样,你的后台任务将不会和一个Activity联系,不会造成内存泄露和不用Activity来消费。我叫这样的对象为“Presenter”。
有那么几种处理后台线程的方法,但都是不可靠的,不过MVP是可靠的。
为什么MVP可以
通过这个视图,显示了不同的应用控件,在发生configuration发生改变或者内存溢出的时候发生了什么。每一个Android开发者都应该知道这个视图,然而这样一个视图并不是每个开发都知道。
Case 1:当用户旋转屏幕,改变语言设置, attache 一个外部显示器,等情况,通常Configuration会发生变化。更多关于[Configuration]
(http://developer.android.com/reference/android/R.attr.html#configChanges)请阅读链接。
Case 2:当用户在开发者设置里面选择了“Don’t keep activities”或者其他Activity到最顶层,Activity会发生restart。
Case 3:没有足够的内存和应用进入后台,process会restart。
最后
现在你可以看到,Fragment当中设置setRetainInstance(true)在这里是没用帮助的,我们只需要设置save/restore就可以。因此,我们可以简单的去除Fragment的setRetainInstance方法,来限制问题的数量。
|Activity, View, Fragment, DialogFragment Static variables and threads | save/restore no change | save/restore reset |
现在看起来爽多了。我们在应用任何情况下,只需要写两段代码就可以完成restore:
·Activity,View,Fragment,DialogFragment的save/restore。·线程restart,restart后台请求。
第一部分,是Android API提供的方法。第二部分是Presenter层的工作。Presenter只要记住那些请求需要被执行,如果一个进程执行期间restart,Presenter将会重新执行它们。
一个例子
这个例子将加载服务器上得数据来显示一些items。如果出现错误将显示一个toast。
我推荐使用RxJava来构建Presenter,因为这个Library控制数据流很简单。
我也要感谢创建The Internet Chuck Norris Database的人,我把它用在了我例子当中。
没用MVP的例子00:
public class MainActivity extends Activity { public static final String DEFAULT_NAME = "Chuck Norris"; private ArrayAdapter<ServerAPI.Item> adapter; private Subscription subscription; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item)); requestItems(DEFAULT_NAME); } @Override protected void onDestroy() { super.onDestroy(); unsubscribe(); } public void requestItems(String name) { unsubscribe(); subscription = App.getServerAPI() .getItems(name.split("\\s+")[0], name.split("\\s+")[1]) .delay(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<ServerAPI.Response>() { @Override public void call(ServerAPI.Response response) { onItemsNext(response.items); } }, new Action1<Throwable>() { @Override public void call(Throwable error) { onItemsError(error); } }); } public void onItemsNext(ServerAPI.Item[] items) { adapter.clear(); adapter.addAll(items); } public void onItemsError(Throwable throwable) { Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show(); } private void unsubscribe() { if (subscription != null) { subscription.unsubscribe(); subscription = null; } }}
一个有经验的开发会发现这个例子是有一些缺陷的:
·每次翻转屏幕都会重新请求一次——每次翻转屏幕用户都会看一会儿空白界面。·如果用户经常翻转屏幕,就会导致内存泄露——每个回调都会保持对MainActivity的引用,并且request运行的时候会把MainActivity保持在内存中。这绝对有可能导致因为内存溢出而应用crash,或者应用运行明显缓慢。
使用MVP的例子01
public class MainPresenter { public static final String DEFAULT_NAME = "Chuck Norris"; private ServerAPI.Item[] items; private Throwable error; private MainActivity view; public MainPresenter() { App.getServerAPI() .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1]) .delay(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<ServerAPI.Response>() { @Override public void call(ServerAPI.Response response) { items = response.items; publish();//onNext } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { error = throwable; publish();//onError } }); } public void onTakeView(MainActivity view) { this.view = view; publish(); } private void publish() { if (view != null) { if (items != null) view.onItemsNext(items); else if (error != null) view.onItemsError(error); } }}
从技术角度讲:MainPresenter有三个线程事件:onNext,onError,onTakeview。通过publish()
方法,onNext或者onError事件会发布到MainActivity实例。
public class MainActivity extends Activity { private ArrayAdapter<ServerAPI.Item> adapter; private static MainPresenter presenter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item)); if (presenter == null) presenter = new MainPresenter(); presenter.onTakeView(this); } @Override protected void onDestroy() { super.onDestroy(); presenter.onTakeView(null); if (isFinishing()) presenter = null; } public void onItemsNext(ServerAPI.Item[] items) { adapter.clear(); adapter.addAll(items); } public void onItemsError(Throwable throwable) { Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show(); }}
MainActivity 创建 MainPresenter,并且保持MainPresenter在onCreate和onDestory周期之外。MainActivity用一个静态变量引用MainPresenter,当由于OOM导致线程重启,MainActivity会检查MainPresenter是否还存在,如果不存在就去创建它。
是的,检查和使用静态变量起来有那么点臃肿,但是稍后我会给大家看如何写的更加优雅。:)
主要思想:
·例子应用不会在每次用户翻转屏幕的时候重新请求。·如果线程被重启,再次加载数据。·当MainActivity被销毁后,MainPresenter不会保持应用
MainActivity实例,这样当旋转屏幕的时候就不会内存泄露,而且也没有取消请求。
Nucleus
Nuleus 是我创建的一个library,灵感来自于Mortarlibrary和Keep It Stupid Simple这篇文章。
下面是功能列表:
·支持在View/Fragment/Activity状态Bundle中save/restore Presenter的状态。Presenter能够存储请求参数到重新启动。·提供一个工具,通过一行代码可以把请求结果和错误放到正确的view当中去,因此无需再写`!=null`来检查。·一个Presenter可以被多个view实例引用。如果Presenter实例使用[Dagger],就不能被多个view引用。·通过一行代码就可以把Presenter和view进行绑定。·提供view的基类:NucleusView, NucleusFragment, NucleusSupportFragment, NucleusActivity。你也可以从他们当中copy代码到任何类当中来利用Nucleus的Presenters。·Presenter在线程重启之后能够自动restart。在`onDestroy`自动取消注册RxJava。·最后,要保持简单明了,让每一个开发者都能够看懂。这里通过大约180行代码来驱动Presenter,230行RxJava代码来支持。
使用Nuleus的例子02
public class MainPresenter extends RxPresenter<MainActivity> { public static final String DEFAULT_NAME = "Chuck Norris"; @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); App.getServerAPI() .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1]) .delay(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .compose(this.<ServerAPI.Response>deliverLatestCache()) .subscribe(new Action1<ServerAPI.Response>() { @Override public void call(ServerAPI.Response response) { getView().onItemsNext(response.items); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { getView().onItemsError(throwable); } }); }}@RequiresPresenter(MainPresenter.class)public class MainActivity extends NucleusActivity<MainPresenter> { private ArrayAdapter<ServerAPI.Item> adapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item)); } public void onItemsNext(ServerAPI.Item[] items) { adapter.clear(); adapter.addAll(items); } public void onItemsError(Throwable throwable) { Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show(); }}
正如你看到的,这个例子明显比前一个例子短,并且简洁。Nucleus用来创建,销毁,保存Presenter, attache 或 detached一个view到Presenter,并且把请求结果自动发送到被 attache 的View当中。
MainPresenter
的代码比较少是因为通过deliverLatestCache()
操作,期延迟了数据和错误,直到view是可用的,才会把数据和错误送到view里。它把数据缓存到内存中,这样当configuration改变的时候,数据还是可用的。
MainActivity
的代码比较少是因为Presenter的创建由NucleusActivity
来管理。所有你需要绑定presenter的类,只需要在类上声明@RequiresPresenter(MainPresenter.class)
注释。
警告!注释!在Android世界中,如果你使用注释,这是一个很好的做法,这不会降低性能。我已Galaxy S(2010的设备)作为基准测试,处理注释只会花去0.3ms。这种注视只会发生在view中,所以我认为注释是对系统性能没有消耗的。
更多例子
这是一个参数持久性的例子。测试列子。
deliverLatestCache()
方法
这是RxPresenter的一个方法,它有三种版本:
·deliver()
延迟onNext,onError和onComplete到view变成可用的才会释放。当你做一次请求的时候可以使用它,例如登录到web service。Javadoc
·deliverLatest()
如果有一个新的onNext可用,将会抛弃老的onNext。如果你有数据需要更新,这将不会积累没有必要的数据。Javadoc
·deliverLastestCache()
和deliverLatest()
比较相似,它保存最后一次数据在内存中,当另一个view变成可用的(例如:configuration 改变),它将重新发送数据到view。如果你不想save/restore请求结果到你的view中(返回结果比较大或者不方便存储到Bundle中),这个方法将允许你去做出更好的用户体验。Javadoc
Presenter的生命周期
Presenter的生命周期与Android的控件相比,明显少一些。
·void onCreate(Bundle savedState)
- 当Presenter被创建的时候会被调用。Javadoc
·void onDestroy()
- 离开view的时候会被调用。Javadoc
·void onSave(Bundle state)
- 当View的onSaveInstanceState
被调用时会调用,保持Presenter的状态。Javadoc
·void onTakeView(ViewType view)
-在Activity或者Fragment调用onResume()
,或者在android.view.View#onAttachedToWindow()
期间。 Javadoc
·void onDropView()
- Activity或者Fragment调用onPause()
,或者在android.view.View#onDetachedFromWindow()
期间。Javadoc
View回收和View栈
通常你的view(Fragment和自定义view)在与用户的交互下随机 attache 和 detached。每次view被 detached的时候不去销毁Presenter,这可能是一个好主意。你可以任何时间 detached和 attache view,presenter会比这些动作活的更持久,继续后台的工作。
联想到view的回收,有个问题:fragment无法知道是否因为配置改变或者被弹出栈被 detached。
Nucleus的意见是:销毁presenter只能发生在view的onDetachedFromWindow()/onDestroy()
并且activity是finish的。所以,如果你销毁view是在正常的activity生命周期,你可发出信号来通知presenter也应该被销毁。这里有两个方法可以用NucleusLayout.destroyPresenter()
和NucleusFragment.destroyPresenter()
。
举个例子,下面是我在我的项目里面如何管理Fragment pop()
操作:
fragment = fragmentManager.findFragmentById(R.id.fragmentStackContainer); fragmentManager.popBackStackImmediate(); if (fragment instanceof NucleusFragment) ((NucleusFragment)fragment).destroyPresenter();
你可以对replace Fragment做类似的操作。压栈操作的时候也可以。
每次view从Activity detached的时候,你可以决定去销毁presenter来避免这个问题,但是你也将在view被detach的时候失去后台任务。
因此,view回收这部分,完全取决于你。也许,我会找到更好的解决方案,如果你知道,请告诉我。
最佳实践
把你的请求参数放在Presenter里
这个规则很简单:主要是为了管理请求。所以view自己不应该掌控和重启请求。从View的角度来看,后台任务,永远不会消失,不需要任何回调也会返回一个结果或错误。
public class MainPresenter extends RxPresenter<MainActivity> { private String name = DEFAULT_NAME; @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); if (savedState != null) name = savedState.getString(NAME_KEY); ... @Override protected void onSave(@NonNull Bundle state) { super.onSave(state); state.putString(NAME_KEY, name); }
我建议使用Icepicklibrary。无需使用注解,就可以减少代码量,并且简化应用逻辑——这一切都发生在编译过程中。可以配合ButterKnife使用。
public class MainPresenter extends RxPresenter<MainActivity> { @Icicle String name = DEFAULT_NAME; @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); Icepick.restoreInstanceState(this, savedState); ... @Override protected void onSave(@NonNull Bundle state) { super.onSave(state); Icepick.saveInstanceState(this, state); }
如果你有超过2个的请求参数,这个library会存储它们。你可以创建一个BasePresenter
,并且把Icepick放在类里,这样所有的子类将会获得@Icicle
,无需再次实现onSave
。这也工作在activity,Fragment和view。
在onTakeView主线程中,执行一个即时查询Javadoc
有时候,你要查询一段小数据,例如从数据库中读取一小段数据。虽然你可以用Nucleus简单的创建一个请求,但是你不必到处使用Nucleus。如果在一个Fragment创建的过程中创建一个后台请求,用户会看到一个空白屏幕一小会儿,尽管这个请求就几毫秒。因此,为了是代码更简短,更友善,使用主线程吧。
不要尝试用Presenter控制你的View
这么做不是个好方式——应用的逻辑会变得更复杂,这是不正常的方式。
正常的方式是,控制流应该是从用户,通过View,到Presenter,再到Model。用户是控制应用程序的一个来源。因此我们的控制流应该是从用户开始,而不是从应用的内部的结构。
当控制流是从View到Presenter,然后Presenter到Model,这是一个线性流,这样很好写代码。这样你得到了一个简单的序列,user->view->presenter->model->data。但是,当控制流是这个样子的:user->view->presenter->view->presenter->model->data,这只是违反了KISS原则。
Fragment?是的,Fragment有时候会违反正常的控制流。他们太复杂了。这里有一篇不错的文章,关于思考Fragment:Advocating Against Android Fragments。但是Flow也没有简化太多。
MVC
如果你熟悉MVC,别用了。MVC完全不同于MVP,MVC并没有解决开发面临的问题。
什么是MVC?
·Model应用内部的逻辑部分。负责数据存储。·View唯一和MVP共同的部分,应用中呈现到屏幕的部分。·Controller输入设备,例如键盘,鼠标,操纵杆。
当你有一台电脑和一个用键盘简单驱动的游戏的时候,MVC出现有很长一段时间了。没有windows,没有图形交互界面,应用程序接收输入(Controller),维持一些状态(Model),产生输出(View)。控制流是这样的:Controller->Model->View。这种模式绝对不能用在Android中。
有很多混淆的MVC模式。人们相信他们使用的是MVC,实际上他们可能用的是MVP(Web开发)。很多Android开发,认为Controller就是控制View,因此他们尝试抽取View的逻辑代码来减少View的代码,用Controller来控制View。我个人没看到这种方式有任何好处。
使用不可变数据结构的复杂关系数据库项目
AutoValue是一个这样的library,在它的描述中写了一堆好处,我推荐看看它。AutoParcel是AutoValue一个Android项目。使用的主要原因是,不用改变对象,通过AutoParcel转换,而不用关心其影响了应用程序的其他部分。他们都是线程安全的。
结尾
尝试MVP,并且分享给你的朋友。:)