ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
原文出处----->[Introduction to Model View Presenter on Android](http://konmik.com/post/introduction_to_model_view_presenter_on_android/),作者[GitHub地址](https://github.com/konmik) ### 译文如下: 本文是从最简单的例子到最佳实践,分步介绍Android上的MVP。文章还介绍了一个新的库,使得Android上的MVP非常简单。 **这很简单吗?我怎样才能使用它?** ### 什么是MVP * **视图**(View)是显示数据并对用户操作做出反应的图层。在Android上,这可能是一个Activity,一个片段,一个android.view.View或一个对话框。 * **模型**(Model)是一个数据访问层,如数据库API或远程服务器API。 * **Presenter**是一个为View提供来自Model的数据的图层。演示者还处理后台任务。 在Android MVP上,将背景任务与活动/视图/片段分开,使其独立于大多数与生命周期相关的事件。这样一个应用程序变得更简单,整体应用程序可靠性提高达10倍,应用程序代码变得更短,代码可维护性变得更好,开发人员的生活变得更加快乐。 ### 为什么在Android上MVP #### 原因1:尽量简单 如果你还没有阅读这篇文章,那么做:[亲吻原则](http://web.archive.org/web/20160206225831/https://people.apache.org/~fhanik/kiss.html) * 大多数现代Android应用程序只使用View-Model架构。 * 程序员参与了View的复杂性而不是解决业务任务。 在你的应用程序中只使用模型视图,你通常会得到“一切都与一切有关”。 ![](https://box.kancloud.cn/d7b36b7a6d24dcb6c58b66bd34b131d9_513x285.png) 如果这个图看起来不复杂,那么想想每个View都会随机消失并出现。不要忘记保存/恢复视图。附加几个后台任务到临时视图,蛋糕准备就绪! “一切与万物有关”的另一种选择是上帝的对象。 ![](https://box.kancloud.cn/5d00bd3b67ffe22a1e3db05cee174b02_608x280.png) 神的目标过于复杂; 其部分不能重复使用,测试或轻松调试和重构。 与MVP ![](https://box.kancloud.cn/dcb97a27d6f39f9efa1a8fd82bce344a_795x262.png) * 复杂的任务分解成更简单的任务,更容易解决。 * 更小的对象,更少的错误,更容易调试。 * 可测试。 MVP视图层变得如此简单,所以在请求数据时甚至不需要回调。查看逻辑变得非常线性。 #### 原因2:后台任务 每当你编写一个活动,一个片段或一个自定义视图,你可以把所有与后台任务连接的方法放到不同的外部或静态类中。这样你的后台任务将不会与Activity连接,不会泄漏内存,也不会依赖Activity的重新创建。我们称这个对象为“演示者”。 有几种不同的方法来处理后台任务,一个正确实施的MVP库是最可靠的。 **为什么这个工作** 下面是一个小图,显示配置更改期间或内存不足事件期间不同应用程序部件发生的情况。每个Android开发人员都应该知道这些数据,但是这个数据却很难找到。 ~~~ | Case 1 | Case 2 | Case 3 |A configuration| An activity | A process | change | restart | restart ---------------------------------------- | ------------- | ------------ | ------------ Dialog | reset | reset | reset Activity, View, Fragment | save/restore | save/restore | save/restore Fragment with setRetainInstance(true) | no change | save/restore | save/restore Static variables and threads | no change | no change | reset ~~~ **案例1**:配置更改通常发生在用户翻转屏幕,更改语言设置,连接外部监视器等情况下。有关此事件的更多信息,请参阅:[configChanges](http://developer.android.com/reference/android/R.attr.html#configChanges)。 **案例2**:当用户在开发者设置中设置了“不要保留活动”复选框,并且另一个活动变为最上层时,会发生活动重新启动。 **情况3**:如果没有足够的内存并且应用程序在后台,则进程重新启动。 **结论** 现在你可以看到,带有setRetainInstance的片段(true)在这里没有帮助 - 我们需要保存/恢复这个片段的状态。所以我们可以简单地扔掉残留的碎片来限制问题的数量。 ~~~ |A configuration| | change, | | An activity | A process | restart | restart ---------------------------------------- | ------------- | ------------- Activity, View, Fragment, DialogFragment | save/restore | save/restore Static variables and threads | no change | reset ~~~ 现在看起来好多了。我们只需要编写两段代码就可以在任何可能的情况下完全恢复应用程序: * 保存/恢复Activity,View,Fragment,DialogFragment; * 在进程重启的情况下重启后台请求。 第一部分可以通过Android API的常规手段完成。第二部分是Presenter的工作。Presenter只记得它应该执行的请求,如果一个进程在执行期间重新启动,Presenter将再次执行它们。 ### 一个简单的例子 这个例子将加载并显示来自远程服务器的一些项目。如果发生错误,会显示一点吐司。 我建议使用[RxJava](https://github.com/ReactiveX/RxJava)来建立主持人,因为这个库允许轻松地控制数据流。 我想感谢那个创造了一个简单的API的人,我用我的例子:[Internet Chuck Norris Database](http://www.icndb.com/) **没有MVP** [例子00](https://github.com/konmik/MVPExamples/tree/master/example00): ~~~ 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的引用,并在请求运行时将其保留在内存中。由于内存不足错误或重要的应用程序减速,绝对有可能导致应用程序崩溃。 **用MVP** [例子01](https://github.com/konmik/MVPExamples/tree/master/example01): 请不要在家里试试这个!:)此示例仅用于演示目的。在现实生活中,你不会使用静态变量来保持演示者。 ~~~ 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(); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { error = throwable; publish(); } }); } 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实例已经提供onTakeView。 ~~~ 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 (!isChangingConfigurations()) 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并将其保持在onCreate / onDestroy周期之外。MainActivity使用静态变量来引用MainPresenter,所以每当进程由于内存不足事件而重新启动时,MainActivity应检查演示者是否仍在此处,并在需要时创建它。 是的,这看起来有点臃肿与检查,并使用静态变量,但后来我会展示如何使这看起来好多了。:) 主要想法是: * 每次用户翻转屏幕时,示例应用程序都不会启动请求。 * 如果一个进程已经重新启动,这个例子再次加载数据。 * MainPresent不保留MainActivity实例的引用,而MainActivity被销毁,所以在屏幕上没有内存泄漏,并且不需要取消订阅请求。 #### 核 Nucleus是我创建的库,同时受到[Mortar库](https://github.com/square/mortar)和[Keep It Stupid Simple](https://people.apache.org/~fhanik/kiss.html)文章的启发。 这里是一个功能列表: * 它支持在View / Fragment / Activity的状态包中保存/恢复Presenter的状态。演示者可以将请求参数保存到该捆绑包中以稍后重新启动它们。 * 它提供了一个工具,只需一行代码即可将请求结果和错误直接引导到视图中,因此您不必编写所有的!= null检查。 * 它允许您有一个以上需要演示者的视图实例。如果你用Dagger(传统的方式)实例化主持人,你不能这样做。 * 它提供了一个快捷方式,只需一行即可将演示者绑定到视图。 * 它提供了基本视图类:NucleusView,NucleusFragment,NucleusSupportFragment,NucleusActivity。您也可以从其中一个复制/粘贴代码,以使您可以使用任何类来利用Nucleus的演示者。 * 它可以在进程重新启动后自动重启请求,并在此期间自动取消订阅RxJava订阅onDestroy。 * 最后,这很简单,任何开发人员都可以理解。(我建议花一些时间潜入RxJava)。只有大约180行代码来驱动Presenter和230行代码,用于RxJava支持。 **例如具有**[核](https://github.com/konmik/nucleus) [例02](https://github.com/konmik/MVPExamples/tree/master/example02) >[info] 注意:自写这篇文章以来,新版本的Nucleus发布了。您可以在Nucleus项目资源库中找到更新的例子。 ~~~ 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创建/销毁/保存演示者,附加/分离View并自动将请求结果发送到附加视图。 MainPresenter代码更短,因为它使用deliverLatestCache()延迟数据源发出的所有数据和错误的操作,直到视图变为可用。它还将数据缓存在内存中,以便在配置更改时重用。 MainActivity由于演示者的创建是由管理的,因此代码更短NucleusActivity。所有你需要绑定一个演示者是写@RequiresPresenter(MainPresenter.class)注释。 警告!一个注释!在Android世界中,如果使用注释,最好检查一下这不会降低性能。我在Galaxy S(2010年设备)上所做的基准测试表明,处理这个注释需要的时间少于0.3毫秒。这只发生在视图的实例化过程中,所以注释被认为是免费的。 **更多的例子** 具有请求参数持久性的扩展示例如下:[Nucleus示例](https://github.com/konmik/nucleus/tree/master/nucleus-example)。 单元测试的一个例子:具有测试的[核心例子](https://github.com/konmik/nucleus/tree/master/nucleus-example-with-tests) **deliverLatestCache() 方法** 这个RxPresenter帮助方法有三个变种: 1. deliver()将延迟所有onNext,onError和onComplete排放,直到View可用。如果您正在进行一次性请求(如登录到Web服务),请使用它。 2. deliverLatest()如果新的onNext值可用,则会放弃较旧的onNext值。如果你有一个可更新的数据源,这将允许你不积累不必要的数据。 3. deliverLatestCache()是一样的,deliverLatest()但它会保持最新的结果在内存中,并将重新交付时,另一个视图的实例变得可用(即在配置更改)。如果您不希望在视图中组织保存/恢复请求结果(如果结果很大或者无法轻易保存到Bundle中),此方法将使您可以更好地体验用户体验。 **演讲者的生命周**期 Presenter的生命周期比其他Android组件的生命周期短得多。 * void onCreate(Bundle savedState) - 每个演示者的创作都会被调用。 * void onDestroy() - 当用户View被破坏而不是因为配置改变而被调用。 * void onSave(Bundle state)- 在View中也会调用onSaveInstanceStatePresenter的状态。 * void onTakeView(ViewType view)- 在活动或片段的onResume()过程中或过程中调用android.view.View#onAttachedToWindow()。 * void onDropView()- 在活动onDestroy()或片段的onDestroyView()过程中或过程中调用android.view.View#onDetachedFromWindow()。 **查看回收和查看堆栈** 通常情况下,您的视图(即片段和自定义视图)在用户交互过程中随机附加和分离。这可能是一个好主意,不要摧毁一个主持人每次一个视图分离。你可以随时分离和附上观点,主持人将胜过所有这些行动,继续后台工作。 #### 最佳做法 **在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); } ~~~ 我建议使用真棒[Icepick库](https://github.com/frankiesardo/icepick)。它减少了代码的大小,简化了应用程序逻辑,而无需使用运行时注释 - 编译过程中所有事情都会发生 这个库是[黄油刀](http://jakewharton.github.io/butterknife)的好伙伴。 ~~~ public class MainPresenter extends RxPresenter<MainActivity> { @State 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); } ~~~ 如果你有更多的请求参数,这个库自然会保存生命。您可以创建IcepickBasePresenter并将其 放入该类,并且所有子类都将自动获得保存其注释的字段的能力@State,您将永远不需要onSave再次实施。这也适用于保存活动,片段或视图的状态。 #### 在主线程中执行即时查询 onTakeView 有时候你有一个简短的数据查询,比如从数据库中读取少量的数据。虽然您可以使用Nucleus轻松创建可重新启动的请求,但您不必在任何地方使用这个强大的工具。如果在创建片段的过程中启动后台请求,则用户将会看到一个空白屏幕,即使该查询只需要几毫秒。所以,为了缩短代码并让用户更快乐,请使用主线程。 #### 不要试图让您的演示者控制您的视图 这样做效果不好 - 应用程序逻辑变得太复杂了,因为它不自然。 自然的方法是通过视图,主持人和模型来实现从用户到数据的控制流。最终用户将使用应用程序,用户是应用程序的控制源。所以控制应该从用户而不是从一些内部的应用程序结构。 当控制从视图到演示者,然后从演示者到模型,这只是一个直接的流程,很容易写这样的代码。你得到一个简单的用户 - >查看 - >演示 - >模型 - >数据序列。但是当控制如下:user - > view - > presenter - > view - > presenter - > model - > data,这只是违反了KISS原则。不要在你的观点和主持人之间打乒乓球。 ### 结论 给MVP一试,告诉朋友。