ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
### **参考**: 原文出处------>[Model-View-Presenter (MVP)](http://hannesdorfmann.com/android/mosby),GitHub地址[Mosby](https://github.com/sockeqwe/mosby),[GitHub](https://github.com/sockeqwe) 译文如下: 译文出处------>[MVP(Model-View-Presenter)模式](http://wiki.jikexueyuan.com/project/notes/Android-Java/MVP.html) [用于android的Model-View-Presenter库](http://hannesdorfmann.com/mosby/) **注意:这里仅供参考,具体问题具体分析** ### **MVP(Model-View-Presenter)模式** [工作流程](http://hannesdorfmann.com/android/mosby/) ![](http://wiki.jikexueyuan.com/project/notes/images/MVP.png) * View通常会持有Presenter的引用; * Presenter持有View和Model的引用; * Model应该包括数据和对数据的获取或者修改操作; #### **具体来说** ![](http://wiki.jikexueyuan.com/project/notes/images/MVP1.png) **invoke:调用,query:查询** * MVP之间应该尽量解耦,可以通过定义接口来实现,相互持有的是接口引用; * View应该尽可能听从Presenter的指令,而不是自己控制,如:接受Presenter的showLoading指令之后才显示正在加载; * 与用户的交互由View负责,显示的细节由View负责; ### **MVP in Android(Mosby)** #### **使用场景**: * 单一的UI单元,例如Fragment,或者一个ViewGroup,原则是这部分UI由一个Presenter独立负责,不必相互干扰; * Acticity、Fragment更应该是View,而不是Presenter; * Core模块集成了Butterknife、FragmentArgs、Icepick等开源库; * MVP模块,定义了MvpPresenter,MvpActivity、MvpFragment类,作为presenter,view的基类; * MvpPresenter是一个接口,有一个MvpBasePresenter实现,使用WeakReference保存View的引用,所以Presenter(子类)要调用View的接口时,需要调用isViewAttached()检查view是否有效,调用getView()获取其引用;(防止内存泄漏) * MvpLceView,模板化loading-content-error流程; * ViewState模块:保存View(并非Android中的View,这里是指MVP中View视图)的状态,处理屏幕旋转、View(Fragment、Activity)重新创建时的状态恢复; * 还有针对Dagger、Retrofit、RX、Testing等的模块; #### [**一些建议**](http://hannesdorfmann.com/android/mosby-playbook/) * Don't “over-architect” In fact refactoring is a vital part of development, you simply can’t avoid it (but you can try to minimize the number of refactoring). Start with doing the simple thing and afterwards refactor and make it more extensible as app and requirements grow. 译文:事实上,重构是开发的一个重要部分,你根本无法避免它(但是你可以尽量减少重构的次数)。从做简单的事情开始,然后重新进行重构,随着应用程序和需求的增长,使其更具扩展性。 * Use inheritance wisely(正确利用继承) 正确利用继承,基类拥有基本特点,子类增加新特性;避免“子类爆炸”; * Don't see MVP as an MVC variant(不要把MVP看成MVC的转变) ![](http://wiki.jikexueyuan.com/project/notes/images/mvp-controller.png) Controller负责控制View(UI)和用户交互时应该执行的动作(调用Presenter的哪个方法);当被Presenter调用显示方法时,如何显示(动效、数据使用方式); - Take separation of Model, View and Presenter serious(严肃的将Model, View and Presenter分离) 写代码的时候,认真思考每一行代码的功能应该属于哪个模块,目前的位置是否合适? - View负责UI显示,以及对UI、用户的响应 - Model负责数据获取、存储、处理 - Presenter负责配合/连接两者 - Presentation Model - 当UI显示对于Model的需求与数据源不一致时:最好的办法是加一层wrapper(包装,装饰);其次是为Model加一些方法,产生需要的域;最差的是为Model加上这些域; - Navigation is a UI thing(其他的一些UI逻辑需要view负责,而不是通过Presenter) 不同界面之间的跳转应该由view负责 - onActivityResult() and MVP 当result仅仅用于显示时,无需涉及presenter;当需要额外处理时,例如图片处理、上传等,则需要放到presenter中去; - MVP and EventBus - A match made in heaven(完美搭配) EventBus常被用于业务逻辑事件、UI更新事件(Fragment之间通信) - 前者presenter负责接收event,控制view显示; - 后者有一种常见做法:Activity实现一个listener接口,让fragment持有该接口引用(attach时赋值),使用该接口进行通信;然而使用EventBus更合适,解耦更彻底; - Optimistic propagation 用户的操作,首先给其成功的反馈,然后在后台进行处理,失败后给出失败的提示,并撤销成功的反馈(显示),这种做法对于用户体验或许更佳; 如何看待内存泄漏:避免activity/fragment的泄漏,但对于presenter,如果希望操作执行完之后再被GC,则subscriber/runnable持有的presenter引用,可以认为是合理的; - MVP scales MVP的V可以是整个屏幕显示的内容,也可以是屏幕上的某个模块显示的内容; - Not every View needs MVP(并不是所有的View视图都要用到MVP模式) 静态的页面并不需要MVP - Only display one Model per MVP view 一个V显示多个M会为代码增加复杂性,尤其是当需要保存ViewState时;合理的做法是将V拆分为独立的V,每个V只负责显示一个M;V可以是fragment,也可以是View/ViewGroup;例子: ![menu-refactored](http://wiki.jikexueyuan.com/project/notes/images/menu-refactored.jpg) - Android Services **Service显然属于业务逻辑部分,由presenter与之通信是合理的**; - Use LCE only if you have LCE Views(Loading-Content-Error (LCE)加载内容错误) LCE还包含loadData,setData,如果没有这两个语义,就不要使用LceView,因为对接口的空实现,有违接口的规范; - Writing custom ViewStates 当需要保存View的状态时,定义好ViewState,并在View内维护、检查; - Testing custom ViewState View和ViewState都是纯Java代码,可以使用Java的单元测试方法进行测试; - ViewState variants 有可能View既显示了数据,又在进行刷新操作,但是ViewState始终只会处于一个状态,通常的做法是为原来的状态再加一层修饰,加以区分;也可以定义一个新的ViewState; - Not every UI representation is a ViewState 例如:显示空内容,并不是新状态,而是show content,只不过content为空; - Not every View needs a ViewState mosby的ViewState通过SavedInstance保存,限制大小为1MB;另外保存的数据有可能是过时的,需要注意; - Avoid Fragments on the backstack and child fragments Fragment的生命周期中可能会有一些问题,所以尽量避免将Fragment放入back stack(返回栈),或者子Fragment; - Mosby supports MVP for ViewGroups - Mosby provides Delegates 好处: - 可以把MVP模式应用到mosby未包含的类中,例如DialogFragment;甚至是第三方库; - 实现自定义的Delegate,可以改变默认Delegate的行为; #### **关于Mosby的一些补充** **核心模块** - 两个基类:MosbyActivity和MosbyFragment。 - 它们是所有其他Activity或Fragment子类的基本类(基础)。 - 两者都使用众所周知的注释处理器([annotation processors](http://hannesdorfmann.com/annotation-processing/annotationprocessing101))来减少写入样板代码。 - MosbyActivity和MosbyFragment使用[Butterknife](http://jakewharton.github.io/butterknife/)进行视图“注入”,[Icepick](https://github.com/frankiesardo/icepick)用于保存和恢复实例状态到一个bundle和FragmentArgs用于注入片段参数。你不必像`Butterknife.inject(this)`(Butterknife版本不一样,调用方法可能也不一样)那样调用注入方法。这种代码已经包含在MosbyActivity和MosbyFragment中。它只是开箱即用。你唯一需要做的就是在你的子类中使用相应的注释。 - 该核心-模块是**不相关的MVP**。这**只是建立摩天大楼应用程序的基础**。 **MVP - Module** - Mosby的MVP模块**使用泛型来确保类型安全**。所有视图的基类是MvpView。基本上它只是一个空的界面。Presenter的基类是MvpPresenter ~~~ public interface MvpView { } public interface MvpPresenter<V extends MvpView> { public void attachView(V view); public void detachView(boolean retainInstance); } ~~~ - MvpActivity和MvpFragment,它们是MvpViews作为活动和碎片的基类: **MvpActivity.java** ~~~ public abstract class MvpActivity<V extends MvpView, P extends MvpPresenter> extends MosbyActivity implements MvpView { protected P presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); presenter = createPresenter(); presenter.attachView(this); } @Override protected void onDestroy() { super.onDestroy(); presenter.detachView(false); } protected abstract P createPresenter(); } ~~~ **MvpFragment.java** ~~~ public abstract class MvpFragment<V extends MvpView, P extends MvpPresenter> extends MosbyFragment implements MvpView { protected P presenter; @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Create the presenter if needed if (presenter == null) { presenter = createPresenter(); } presenter.attachView(this); } @Override public void onDestroyView() { super.onDestroyView(); presenter.detachView(getRetainInstance()); } protected abstract P createPresenter(); } ~~~ 注意: - 目的:MvpView(即片段或活动)被附加到和分离于MvpPresenter。Mosby需要使用Activities和Fragments生命周期来做到这一点,就像你在上面所看到的代码中看到的一样。通常情况下,Presenter演示者将被绑定到该生命周期。所以在`presenter.onAttach()`和`presenter.onDetach()`中应该完成初始化和清理工作(比如取消异步运行任务)。稍后我们将讨论Presenter演示者如何通过使用`setRetainInstanceState(true)`在Fragments片段中“逃避”这个生命周期。 - MvpPresenter是一个接口。MVP模块提供MvpBasePresenter,这是一个Presenter的实现类,它使用了WeakReference保存被引用的视图(Fragment or Activity),以避免内存泄漏。因此,当演示者想要调用视图的方法时,您必须通过检查`isViewAttached()`并使用`getView()`获取引用来检查View视图是否附加到Presenter。 **Loading-Content-Error (LCE)加载内容错误** 通常Fragment一遍又一遍地做同样的事情。比如:它在后台加载数据,在加载时显示加载视图(即ProgressBar),在屏幕上显示加载的数据或在加载失败时显示错误视图。现在支持拉刷新很容易,因为SwipeRefreshLayout是Android的支持库的一部分。为了**不重复实施这个工作流程**,Mosby的MVP模块提供了MvpLceView: ~~~ public interface MvpLceView<M> extends MvpView { /** * Display a loading view while loading data in background. * <b>The loading view must have the id = R.id.loadingView</b> * * @param pullToRefresh true, if pull-to-refresh has been invoked loading. */ public void showLoading(boolean pullToRefresh); /** * Show the content view. * * <b>The content view must have the id = R.id.contentView</b> */ public void showContent(); /** * Show the error view. * <b>The error view must be a TextView with the id = R.id.errorView</b> * * @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); } ~~~ 可以使用**MvpLceActivity**实现MvpLceView和**MvpLceFragment**实现MvpLceView的那种视图。两者都假设填充的xml布局包含视图与`R.id.loadingView`,`R.id.contentView`和`R.id.errorView`。 **示例**: 我们通过使用CountriesAsyncLoader加载Country列表,并将其显示在RecyclerView的Fragment中。你可以在[这里](https://db.tt/ycrCwt1L)下载[示例APK](https://db.tt/ycrCwt1L)。源码托管在[GitHub上](https://github.com/sockeqwe/mosby/tree/master/sample) 注意的是下面示例中的代码可能与[GitHub上的代码](https://github.com/Alexwsc/mosby/tree/master/sample)不一样,这是因为该文章是作者之前就写过的,里面的依赖库可能已经更新,而且作者写的开源库也在迭代,所以一切以GitHub上的源码为准 首先定义视图界面CountriesView ~~~ public interface CountriesView extends MvpLceView<List<Country>> { } ~~~ **为什么我需要为View定义接口?** 1. 由于它是一个接口,你可以改变View视图的实现。你可以简单的将你的代码从扩展Activity的东西移到扩展Fragment的东西上。 2. 模块化:您可以将整个业务逻辑,Presenter和View Interface移动到独立的库项目中。然后,您可以在包含各种应用程序的Presenter中使用此库。下图显示了左侧使用一个Activity的[kicker APP](https://play.google.com/store/apps/details?id=com.netbiscuits.kicker),而[meinVerein app](https://play.google.com/store/apps/details?id=com.tickaroo.meinverein)使用了嵌入在ViewPager中的Fragment。两者都使用相同的库,其中View-Interface和Presenter被定义和单元测试。 ![](http://hannesdorfmann.com/images/mosby/mvp-reuse.png) 3. 您可以轻松编写单元测试,因为您可以通过实现视图界面来模拟视图。也可以引入演示者的java接口,通过使用模拟演示者对象进行单元测试更加容易。 4. 为视图定义一个接口的另一个非常好的副作用是,你不必直接从演示者Presenter调用activity / fragment(活动/片段)的方法。由于在实现演示者的过程中,您在IDE的自动完成中看到的唯一方法就是视图界面的那些方法,因此您将得到**明确的分离**。从我个人的经历来看,我可以说这是非常有用的,特别是如果你在一个团队工作。 请注意,我们也可以使用`MvpLceView <List <Country >>`而不是定义一个(空的,因为继承方法)接口CountriesView。但是有一个专用的接口界面CountriesView可以提高代码的可读性,而且我们可以更灵活地在将来定义更多的与View有关的方法。 下面是代码 **countries_list.xml** ~~~ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.hannesdorfmann.mosby3.sample.mvp.lce.activity.CountriesActivity"> <include layout="@layout/loading_view" /> <include layout="@layout/error_view" /> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/contentView" android:layout_width="match_parent" android:layout_height="match_parent" > <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout> </FrameLayout> ~~~ **loading_view.xml** ~~~ <?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ProgressBar android:id="@+id/loadingView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:indeterminate="true" /> </merge> ~~~ **error_view.xml** ~~~ <?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/errorView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="16dp" android:layout_gravity="center" android:drawableTop="@drawable/ic_cloud_off" android:drawablePadding="16dp" android:textSize="20sp" android:textColor="#656565" android:text="An error has occurred! click here to retry" android:gravity="center" /> </merge> ~~~ **CountriesPresenter.java**(控制CountriesView并启动CountriesAsyncLoader) ~~~ public class CountriesPresenter extends MvpBasePresenter<CountriesView> { @Override public void loadCountries(final boolean pullToRefresh) { getView().showLoading(pullToRefresh); CountriesAsyncLoader countriesLoader = new CountriesAsyncLoader( new CountriesAsyncLoader.CountriesLoaderListener() { @Override public void onSuccess(List<Country> countries) { if (isViewAttached()) { getView().setData(countries); getView().showContent(); } } @Override public void onError(Exception e) { if (isViewAttached()) { getView().showError(e, pullToRefresh); } } }); countriesLoader.execute(); } } ~~~ **CountriesFragment.java**(实现CountriesView) ~~~ public class CountriesFragment extends MvpLceFragment<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter> implements CountriesView, SwipeRefreshLayout.OnRefreshListener { @InjectView(R.id.recyclerView) RecyclerView recyclerView; CountriesAdapter adapter; @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(); } // Just a shorthand that will be called in onCreateView() @Override protected int getLayoutRes() { return R.layout.countries_list; } @Override public void setData(List<Country> data) { adapter.setCountries(data); adapter.notifyDataSetChanged(); } @Override public void onRefresh() { loadData(true); } } ~~~ 没有太多的代码要写,对吧?这是因为基类 MvpLceFragment已经实现了从加载视图切换到内容视图或错误视图。乍一看,MvpLceFragment的泛型参数列表` MvpLceFragment<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter>`可能会阻止你。让我解释一下:第一个泛型参数是内容视图的类型。第二个是用这个片段显示的模型。第三个是View界面,最后一个是Presenter的类型。总结一下:**MvpLceFragment <AndroidView,Model,View,Presenter>**。 另一件您可能已经注意到的是**getLayoutRes()**,它是在MosbyFragment中引入的用于扩充 xml视图布局的简写: ~~~ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ return inflater.inflate(getLayoutRes(), container, false); } ~~~ 所以,而不是覆盖**onCreateView()**你**可以重写getLayoutRes()**。 一般来说,**onCreateView()应该只创建视图,而onViewCreated()应该被重写来初始化就像Adapter for RecyclerView**。 **重要提示:不要忘记调用super.onViewCreated()** #### **ViewState - Module** 现在你应该知道如何使用Mosby。Mosby的ViewState模块可以帮助您解决Android开发中令人讨厌的事情:处理屏幕方向更改。 问题:如果我们将设备从纵向旋转到横向,运行我们的国家/地区示例应用程序,并且已经显示国家,那么会发生什么呢? 答:新的CountriesFragment被实例化和显示应用程序启动时进度(和负载再次国家名单),而不是显示在各国家的名单RecyclerView(因为它是屏幕旋转之前),如下图所示 ![](https://box.kancloud.cn/4149fd188870c2a104530b0bb3a41ed8_489x600.gif) **Mosby介绍ViewState来解决这个问题** 我们的想法是,我们跟踪presenter演示者调用的在附加的View上的方法。例如,presenter演示者调用view.showContent()。一旦showContent()被调用,视图就知道它的状态是“显示内容”,因此View视图将这些信息存储到ViewState中。如果视图取向被改变期间而受到破坏时,ViewState会被存储在Bundle(在**Activity.onSaveInstanceState(Bundle**)或**Fragment.onSaveInstanceState(Bundle**)得到的Bundle)中;将在**Activity.onCreate(Bundle**)或**Fragment.onActivityCreated(Bundle**)中恢复。 由于并不是每一种数据(我谈到的数据类型作为参数在**view.setData(**)传递)都可以存储在一个Bundle,不同的ViewState实现提供像ArrayListLceViewState类型的数据`ArrayList <Parcelable>`,ParcelableDataLceViewState——像 用于Parcelable或SerializeableLceViewState类型的数据——用于Serializeable类型的数据。如果你使用一个保留的碎片Fragment(更多关于保留下面的碎片),那么ViewState不会在屏幕方向变化时被破坏,也不需要保存到一个Bundle中。因此它可以存储任何类型的数据。 在这种情况下,你应该使用**RetainingFragmentLViewViewState**。恢复ViewState很容易。由于我们有一个干净的体系结构和View的接口,ViewState可以通过调用与演示者相同的接口方法来恢复关联的视图。例如**MvpLceView**基本上有3个状态:它可以显示showContent(),showLoading()和showError(),因此ViewState自己调用相应的方法来恢复视图状态。 这只是内部。你只需要知道如果你想编写自己的自定义ViewStates。使用ViewStates非常简单。实际上,要将**MvpLceFragment**迁移到**MvpLceViewStateFragment**,只需另外实现**createViewState(**)和**getData(**)。让我们为**CountriesFragmen**t做: ~~~ public class CountriesFragment extends MvpLceViewStateFragment<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter> implements CountriesView, SwipeRefreshLayout.OnRefreshListener { @InjectView(R.id.recyclerView) RecyclerView recyclerView; CountriesAdapter adapter; @Override public LceViewState<List<Country>, CountriesView> createViewState() { return new RetainingFragmentLceViewState<List<Country>, CountriesView>(this); } @Override public List<Country> getData() { return adapter == null ? null : adapter.getCountries(); } // The code below is the same as before @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(); } // Just a shorthand that will be called in onCreateView() @Override protected int getLayoutRes() { return R.layout.countries_list; } @Override public void setData(List<Country> data) { adapter.setCountries(data); adapter.notifyDataSetChanged(); } @Override public void onRefresh() { loadData(true); } } ~~~ 就这样。您不必更改演示者的代码或其他内容。下图就是改变代码后的示意图,可以看到现在视图在方向更改后仍处于相同的“状态”,即视图显示纵向国家列表,然后显示国家/地区列表景观。该视图显示横向刷新指示器,并显示更改为纵向后刷新指示器。 ![](https://box.kancloud.cn/cdabbda4ccde3385e284e51436c28ca5_599x595.gif) **编写自己的ViewState** ViewState是一个非常强大和灵活的概念。到目前为止,您已经学会了使用提供的LCE(加载内容错误)ViewsStates中的一个是多么容易。现在让我们写自己的自定义视图和ViewState。我们认为应该只显示两个不同种类的数据对象的A和B。结果应该是这样的: ![](https://box.kancloud.cn/c03751ced2e09706b44b9c980781421c_515x626.gif) View接口和数据对象(模型)如下所示: ~~~ public class A implements Parcelable { String name; public A(String name) { this.name = name; } public String getName() { return name; } } public class B implements Parcelable { String foo; public B(String foo) { this.foo = foo; } public String getFoo() { return foo; } } public interface MyCustomView extends MvpView { public void showA(A a); public void showB(B b); } ~~~ 在这个简单的示例中,我们没有任何业务逻辑。让我们假设在现实世界的应用程序将有我们的业务逻辑,产生复杂的操作一个A或B。我们的presenter 看起来像这样: ~~~ public class MyCustomPresenter extends MvpBasePresenter<MyCustomView> { Random random = new Random(); public void doA() { A a = new A("My name is A "+random.nextInt(10)); if (isViewAttached()) { getView().showA(a); } } public void doB() { B b = new B("I am B "+random.nextInt(10)); if (isViewAttached()) { getView().showB(b); } } } ~~~ **MyCustomActivity.java**(implements MyCustomView) ~~~ public class MyCustomActivity extends MvpViewStateActivity<MyCustomView, MyCustomPresenter> implements MyCustomView { @InjectView(R.id.textViewA) TextView aView; @InjectView(R.id.textViewB) TextView bView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.my_custom_view); } @Override public RestoreableViewState createViewState() { return new MyCustomViewState(); // Our ViewState implementation } // Will be called when no view state exist yet, // which is the case the first time MyCustomActivity starts @Override public void onNewViewStateInstance() { presenter.doA(); } @Override protected MyCustomPresenter createPresenter() { return new MyCustomPresenter(); } @Override public void showA(A a) { MyCustomViewState vs = ((MyCustomViewState) viewState); vs.setShowingA(true); vs.setData(a); aView.setText(a.getName()); aView.setVisibility(View.VISIBLE); bView.setVisibility(View.GONE); } @Override public void showB(B b) { MyCustomViewState vs = ((MyCustomViewState) viewState); vs.setShowingA(false); vs.setData(b); bView.setText(b.getFoo()); aView.setVisibility(View.GONE); bView.setVisibility(View.VISIBLE); } @OnClick(R.id.loadA) public void onLoadAClicked() { presenter.doA(); } @OnClick(R.id.loadB) public void onLoadBClicked() { presenter.doB(); } } ~~~ 由于我们没有LCE(Loading-Content-Error),我们没有使用MvpLceActivity作为基类。我们使用MvpViewStateActivity作为基类,这是支持ViewState的最一般的Activity实现。基本上,我们认为简单地显示aView或bView。**在onNewViewStateInstance()中,我们必须指定在第一个Activity开始时要做什么,因为没有以前的ViewState实例存在要恢复**。在showA(A a)和showB(B b)中,我们必须将显示A或B的信息保存到ViewState中。我们差不多完成了 MyCustomViewState实现丢失: ~~~ public class MyCustomViewState implements RestoreableViewState<MyCustomView> { private final String KEY_STATE = "MyCustomViewState-flag"; private final String KEY_DATA = "MyCustomViewState-data"; public boolean showingA = true; // if false, then show B public Parcelable data; // Can be A or B @Override public void saveInstanceState(Bundle out) { out.putBoolean(KEY_STATE, showingA); out.putParcelable(KEY_DATA, data); } @Override public boolean restoreInstanceState(Bundle in) { if (in == null) { return false; } showingA = in.getBoolean(KEY_STATE, true); data = in.getParcelable(KEY_DATA); return true; } @Override public void apply(MyCustomView view, boolean retained) { if (showingA) { view.showA((A) data); } else { view.showB((B) data); } } /** * @param a true if showing a, false if showing b */ public void setShowingA(boolean a) { this.showingA = a; } public void setData(Parcelable data){ this.data = data; } } ~~~ 正如你所看到的,我们必须保存我们的ViewState通过方法saveInstanceState(),一个从Activity.onSaveInstanceState()被调用的方法,而且恢复视图状态的数据通过方法restoreInstanceState(),一个被调用从Activity.onCreate()的方法 。apply()方法从activity被调用来恢复view state视图状态。我们通过像presenter那样调用相同的View接口方法showA()或showB()来做到这一点。 这个外部的ViewState类将复杂度和责任从视图状态恢复到这个分离的类中。编写ViewState类的单元测试比编写Activity类更容易。 #### **如何处理后台线程?** Presenter通常会观察到后台线程。有两种情况下演示者可以处理后台线程取决于周围的Activity或Fragment: **Retaining Fragment(保留片段)** 保留片段:如果设置了Fragment.setRetainInstanceState(true),那么片段在屏幕旋转过程中不会被破坏。只有片段的GUI( 从onCreateView()返回的android.view.View)被销毁(一个新创建的)。这意味着所有的片段类成员变量在屏幕旋转之后仍然存在,所以屏幕方向已经改变之后,演示者仍然在那里。在这种情况下,我们只需将主视图中的旧视图分开,并将新视图添加到演示者。因此,演示者不必取消任何正在运行的后台任务,因为视图被重新连接。例: 1. 我们开始我们的应用程序在某一个方向。 2. 保留片段被实例化并调用onCreate(),onCreateView(),createPresenter(),还通过调用presenter.attachView()将视图(片段本身)附加到演示者。 3. 接下来,我们将设备从纵向旋转到横向。 4. onDestroyView()被调用,调用presenter.detachView(true)。请注意,参数为true,通知演示者片段是保留片段(否则参数将设置为false)。因此,演示者知道他不必取消正在运行的后台线程。 5. 应用程序现在在布景(绘制)。onCreateView()被调用,但不是 createPresenter(),因为 presenter!= null,因为presenter变量因为setRetainInstanceState(true)而在方向更改中存活。 6. 视图被presenter.attachView()重新连接到演示者。 7. ViewState被恢复。由于没有后台线程被取消,所以不需要重启后台线程。 **Activity and NOT Retaining Fragments(Activity和不保留的Fragments)** 活动和不保留碎片:在这种情况下,工作流程非常简单。一切都被破坏(演示者实例),因此演示者应该取消正在运行的后台任务。例: 1. 我们在一个方向用一个非保留片段开始我们的应用程序 2. 该片段被实例化和调用的onCreate() ,onCreateView() ,createPresenter() ,并通过调用附加视图(片段),以演示presenter.attachView() 。 3. 接下来,我们将设备从纵向旋转到横向。 4. onDestroyView()被调用,调用presenter.detachView(false)。演示者取消后台任务。 5. onSaveInstanceState(Bundle)在ViewState被保存到Bundle的地方被调用。 6. 应用程序现在在布景(绘制)。一个新的Fragment被实例化并调用onCreate(),onCreateView(),createPresenter(),它创建一个新的演示者实例,并通过调用presenter.attachView()将新视图附加到新的演示者。 7. ViewState从Bundle中恢复并恢复视图状态。如果ViewState是showLoading,则演示者重新启动新后台线程以加载数据。 总结这里是ViewState支持活动的生命周期图: ![](http://hannesdorfmann.com/images/mosby/mvp-activity-lifecycle.png) 以下是具有ViewState支持的Fragments的生命周期图: ![](http://hannesdorfmann.com/images/mosby/mvp-fragment-lifecycle.png) ### **Retrofit - Module** Mosby提供**LceRetrofitPresenter**和**LceCallback**。编写一个演示程序以支持LCE方法**showLoading**(),**showContent**()和**showError**()可以用几行代码完成。 ~~~ public class MembersPresenter extends LceRetrofitPresenter<MembersView, List<User>> { private GithubApi githubApi; public MembersPresenter(GithubApi githubApi){ this.githubApi = githubApi; } public void loadSquareMembers(boolean pullToRefresh){ githubApi.getMembers("square", new LceCallback(pullToRefresh)); } } ~~~ ### **Dagger - Module** 建立一个没有依赖注入的应用程序?泰德莫斯比会踢你的屁股!**Dagger是Java中使用最多的依赖注入框架之一,并且非常受Android开发人员的欢迎**。 莫斯比支持[Dagger1](https://github.com/square/dagger)。Mosby提供了一个名为**getObjectGraph**()的方法的Injector接口。通常,你有一个应用程序范围的模块。要轻松地共享此模块,您必须继承**android.app.Application**并使其实现Injector。然后所有的Activities和Fragments都可以通过调用getObjectGraph()来访问ObjectGraph,因为DaggerActivity和DaggerFragment也是Injector。你也可以调用plus(Module)来添加Module,通过覆写Activity或Fragment中的getObjcetGraph()来添加模块。我个人已经迁移到 [Dagger2](https://github.com/google/dagger),它也与Mosby合作。你可以在[Github](https://github.com/sockeqwe/mosby)上找到Dagger1和Dagger2的样本。Dagger1示例apk可以在[这里](https://db.tt/3fVqVdAz)下载,Dagger2示例apk可以在[这里](https://db.tt/z85y4fSY)下载 ### **Rx - Module** Obwarebles ftw!现在所有的酷孩子都使用RxJava,你知道吗?RxJava非常酷!因此,Mosby提供了**MvpLceRxPresenter**,它在内部是一个订阅者,并且会自动处理onNext(),onCompleted()和onError()以及调用相应的LCE方法,如showLoading(),shwoContent()和showError()。它还附带RxAndroid到**observerOn**()Android的主UI线程。您可能认为您不再需要使用RxJava模型视图展示器。那么,这是你的决定。在我看来,View和Model之间的清晰分离是非常重要的。我也认为像ViewState这样的一些不错的功能在没有MVP的情况下并不容易实现。最后但并非最不重要的一点,你是否真的想返回在含有超过1000多行代码的活动和片段中?欢迎回到意大利面代码地狱。好吧,让我们公平一点,这不是意大利面代码,因为Observables引入了一个很好的结构化工作流程,但是更接近于将Activity或Fragment设置为[BLOB](http://www.antipatterns.com/briefing/sld024.htm) ### **Testing - Module(测试 - 模块)** 您可能已经注意到存在测试模块。该模块在内部用于测试mosby库。不过,它也可以用于你自己的应用程序。它使用Robolectric为您的LCE Presenter,Activities和Fragments提供单元测试模板。基本上,它检查被测试的演示者是否工作正常:演示者是否调用showLoading(),showContent()和showError()。您也可以验证setData()中的数据。所以你可以为Presenter和底层编写一个黑盒测试。Mosby的测试模块还提供了测试**MvpLceFragment**或**MvpLceActivity**的可能性。这是一种“精简”UI测试。这些测试只检查片段或活动是否正常工作,而不会崩溃,检查xml布局是否包含所需的id,如R.id.loadingView,R.id.contentView和R.id.errorView,或者检查loadView是否可见,加载数据时,错误视图是否可见,内容视图是否可以处理由setData()提交的已加载数据。这不像你用Espresso做的UI测试。我没有看到需要为LCE Views编写UI测试。总结这里是泰德莫斯比的测试技巧: 1. 编写传统的单元测试来测试您的业务逻辑和模型。 2. 使用MvpLcePresenterTest测试您的演示者, 3. 使用MvpLceFragmentTest和MvpLceActivityTest来测试你的MvpLceFragment / Activity。 4. 如果你愿意,你可以使用Espresso来编写UI测试。 更新:通过展示如何在Android上实现邮件客户端的新博客文章,提供与Mosby相关的一些技巧在线:[Stinson的Mosby参考文档](http://hannesdorfmann.com/android/mosby-playbook)