Ted Mosby - 軟件架構
- 作者:Hannes Dorfmann
- 原文鏈接 : [http://hannesdorfmann.com/android/mosby/]
(http://hannesdorfmann.com/android/mosby/) - 文章出自 : Android開發技術前線
- 譯者 : Mr.Simple
我給這篇關於Android庫的博客起的名字靈感來源於《老爸老媽浪漫史》中的建築設計師Ted Mosby。這個Mosby庫可以幫助大家在Android上通過Model-View-Presenter模式做出一個完善穩健、可重複使用的軟件,還可以藉助ViewState輕鬆實現屏幕翻轉。
Model-View-Presenter (MVP)
MVP模式是一個把view從低層模型分離出來的一種現代模式。MVP由model–view–controller (MVC)軟件模式衍生而來,常用於構建UI
- MVP中的M(model)代表的是將會顯示在view(UI)中的數據。
- MVP中的V(view)是顯示數據(model)並且將用戶指令(events)傳送到presenter以便作用於那些數據的一個接口。View通常含有Presenter的引用。
- MVP中的P(presenter)扮演的是“中間人”的作用(就如MVC中的controller),且presenter同時引用view和model。值得注意的是,“Model”這個詞並不正確。嚴格意義上來說,它指的應該是檢索或控制一個Model的業務邏輯層。舉個例子,比如你的數據庫裏面包含了User,而你的View想要顯示一個User列表,那麼Presenter會引用數據庫中的業務邏輯層(比如DAO)從而查詢到一個User列表。如圖1-1.
從數據庫中查詢或顯示User列表的具體流程如圖1-2:
以上工作流程圖應該能夠說明問題了。但是,還有以下幾點值得注意的地方:
-
Presenter不是一個OnClickListener。View主要是負責處理用戶輸入並調用presenter相應的方法。那麼問題來了,爲什麼不把Presenter 直接做成一個OnClickListener,從而把“轉發流程”給省略掉呢?大家想想,如果這樣做的話,首先,presenter需要知道view的內部構件。舉個例子,如果一個View有兩個按鈕,且這個view在這兩個按鈕上都把Presenter 註冊成OnClickListener的話,那麼發生點擊事件時Presenter (在不知道view中按鈕引用等內部構件的情況下)怎麼能夠區分出是哪一個按鈕被點擊了呢?Model,View和Presenter三者應解耦。其次,如果讓Presenter 執行OnClickListener,Presenter就被綁定到了Android平臺上。理論上來說presenter和業務邏輯層都是純舊式的能夠與桌面應用或其他任何java應用共享的java代碼。
-
大家在第1步和第2步中可以看到,View 只執行Presenter 指示的操作:用戶點擊“load user button”(第1步)後,view並沒有直接顯示加載動畫,而是在第2步presenter明確告訴其顯示加載動畫後才顯示的。這一Model-View-Presenter的變體稱之爲MVP 被動視圖。這個view可以說是要多笨有多笨。這時我們需要讓presenter以一種更抽象的方式來控制view。比如,presenter在調用 view.showLoading() 時並不控制view的諸如動畫等具體事項。所以presenter不應調用view.startAnimation() 等方法。
-
通過執行MVP被動視圖,併發性以及多線程更容易處理。大家可以看到,第3步中數據庫查詢異步運行,並且presenter作爲Listener/Observer,在數據準備顯示時presenter收到通知。
Android上的MVP
目前爲止一切順利。但是大家怎麼樣把MVP運用到自己的Android 應用上呢?第一個問題在於,我們要把MVP模式運用到什麼地方?Activity上、Fragment上、還是像RelativeLayout這類的ViewGroup上?我們來看看Android平板上的Gmail應用,如圖1-3:
在我看來,上圖屏幕中有四個可以使用MVP的地方。我所說的“可以使用MVP的地方”是指屏幕上顯示的、在邏輯上屬於一個整體的UI元素。因此這些地方也可以稱爲是可以運用MVP的一個單獨的UI單元。如圖 1-4.
看起來MVP似乎很適合運用到Activity,特別是Fragment上。通常Fragment只負責顯示單一的如ListView之類的內容,就像依靠MailProvider 來獲取一系列Mails的InboxPresenter 控制下的 InboxView一樣。但是,MVP不僅僅限於Fragment或Activity,它還可以運用到SearchView中顯示的ViewGroup中。在我的大多數app裏面我都在Fragment運用MVP模式。但是大家可以自行決定把MVP運用到什麼地方,前提是view是獨立的,這樣這樣presenter才能在不與其他Presenter衝突的情況下控制View。
我們爲什麼要實現MVP?
我們如何在不使用MVP模式時顯示Email列表到Fragment? 通常,我們需要獲取並且合併本地SQL數據庫和從IMAP郵件服務器獲取的郵件列表,然後將郵件列表綁定到收件箱view中。那麼,此時fragment的代碼又會是怎麼樣的呢?我們需要運行兩個AsyncTasks 並實現一個“等待機制”(等到兩個任務將兩者的加載數據合併到一個單獨的mail列表)。我們還需要注意的是在加載時要顯示加載動畫(ProgressBar),之後用ListView替代。我們需要把所有的代碼放到Fragment中嗎?要是加載過程中出現錯誤怎麼辦?屏幕翻轉怎麼辦?誰來負責撤銷AsyncTasks ?這一系列的問題都可以通過MVP得到解決。讓我們跟那些帶有上千行大雜燴代碼的activity和fragment說拜拜吧
但是,在我們深入研究如何將MVP運用到Android中之前,我們需要弄清楚的一個問題是:Activity或Fragment究竟是一個View還是一個Presenter。Activity或Fragment似乎既是View也是Presenter,因爲它們都有 onCreate() 或onDestroy()之類的生命週期回調功能,並且它們負責從一個UI控件到另一個UI 控件的轉換(比如在加載時顯示ProgressBar,然後顯示帶有數據的ListView)等View操作。大家可能會覺得這裏的Activity或Fragment就是一個Controller,我猜可能也是這麼一個初衷。但是在經歷了幾年的Android應用開發之後,我得出這麼一個結論:我們應該把Activity或Fragment看作是一個不太智能的View,而不是把它們看作一個Presenter。後文我會給出原因。
綜上,我想給大家介紹一個在Android平臺上開發基於MVP的應用的一個 Mosby庫。
Mosby
大家可以在Github和Maven Central上找到Mosby庫。Mosby分爲幾個子模塊,大家可以根據自己的需要選取組件。我們來回顧一下最重要的一個模塊。
核心模塊 ( Core Module)
《老爸老媽浪漫史》中的建築設計師Ted Mosby想建造一棟摩天大樓。而建造這樣一棟宏偉的建築必須打好堅實的地基。這對Android應用的開發來說是也是一樣的道理。基本上,Core Module 分爲兩種類型:MosbyActivity 和MosbyFragment。這兩者是所有其他activity或fragment子類的基類(相當於建築的地基)。兩者都使用我們大家所熟知的APT (Annotation Processing Tool)來減少一些樣板式代碼。MosbyActivity 和MosbyFragment 使用Butterknife進行view的注入,使用Icepick 將實例狀態保存和存儲到Bundle中,使用FragmentArgs注入Fragment參數。我們不需要再調用Butterknife.inject(this)等插入方法。這類代碼已經包含在了MosbyActivity 和 MosbyFragment中。它是即時可用的。我們需要做的就是使用子類中相應的註解。核心模塊與MVP沒有關聯,它只是寫一個大型軟件的基礎。
MVP模塊( MVP Module )
Mosby庫中的MVP模塊使用泛型來確保類型安全。所有view的基類是MvpView。從根本上說這只是一個空的interface 。Presenter的基類是MvpPresenter:
<code class="hljs axapta has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">interface</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpView</span>{</span>} <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">interface</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpPresenter</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">V</span> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span></span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpView</span>>{</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> attachView(V view); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> detachView(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> retainInstance); }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul>
上文提到,我們把Activity和Fragment看做View。因此Mosby庫的MVP模塊提供了 屬於MvpViews 的MvpActivity和MvpFragment作爲Activity和Fragment的基類。
<code class="hljs axapta has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">abstract</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpActivity</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">P</span> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span></span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpPresenter</span>> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span></span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MosbyActivity</span> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span></span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpView</span>{</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> P presenter; @Override <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> onCreate(Bundle savedInstanceState){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onCreate(savedInstanceState); presenter = createPresenter(); presenter.attachView(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onDestroy(); presenter.detachView(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">false</span>); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">abstract</span> PcreatePresenter(); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">abstract</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpFragment</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">P</span> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span></span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpPresenter</span>> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MosbyFragment</span> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span></span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpView</span>{</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> Ppresenter; @Override <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> onViewCreated(View view,@Nullable Bundle savedInstanceState){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onViewCreated(view,savedInstanceState); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Create the presenter if needed</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span>(presenter == <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>){ presenter = createPresenter(); } presenter.attachView(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>); } @Override <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> onDestroyView(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onDestroyView(); presenter.detachView(getRetainInstance()); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">abstract</span> PcreatePresenter(); } } @Override <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> onDestroy(){ </code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li></ul>
這一理念主要是一個MvpView (也就是Fragment or Activity)會關聯一個MvpPresenter,並且管理MbpPresenter的聲明週期。大家從上面的代碼片段可以看到,Mosby使用Activity和Fragement生命週期來實現這一目的。通常presenter是綁定在該生命週期上的。所以初始化或者清理一些東西等操作(例如撤銷異步運行任務)應該在 presenter.onAttach()和 presenter.onDetach()上進行。我們稍後會談到presenter如何使用setRetainInstanceState(true) “避開”Fragment中的生命週期。我相信大家也注意到了, MvpPresenter是一個interface 。MVP模塊提供一個MvpBasePresenter,這個MvpBasePresenter只持有View(是一個Fragment或Activity)的弱引用,從而避免內存泄露。因此,當presenter想要調用view方法時,我們需要查看isViewAttached() 並使用getView()來獲取引用,以檢查view是否連接到了presenter。
Loading-Content-Error (LCE)
通常Fragment會一直重複做某一件事。它在後臺加載數據,同時顯示加載view(即ProgressBar),並在屏幕上顯示加載的數據,或者當加載失敗時顯示view錯誤。如今,下拉刷新支持很容易實現,因爲SwipeRefreshLayout是Android支持庫的組成部分。爲了避免重複執行這一工作流,Mosby庫的MVP模塊提供了MvpLceView。
<code class="hljs scala has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">public interface MvpLceView<M> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> MvpView{ <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * 顯示一個加載中的視圖 * loading view 必須有個id 爲 R.id.loadingView的View * <span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;">@param</span> pullToRefresh 如果是true,那麼表示下拉刷新被觸發了 */</span> public void showLoading(boolean pullToRefresh); <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * 顯示 content view. * <content view 的id必須是R.id.contentView */</span> public void showContent(); <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * 顯示錯誤信息 * <span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;">@param</span> e The Throwable that has caused this error * <span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;">@param</span> pullToRefresh true, if the exception was thrown during pull-to-refresh, otherwise * false. */</span> public void showError(Throwable e,boolean pullToRefresh); <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * The data that should be displayed with {<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;">@link</span> #showContent()} */</span> public void setData(M data); }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li></ul>
針對那種類型的view我們可以採用 MvpLceActivity implements MvpLceView 和 MvpLceFragment implements MvpLceView。兩者均假設解析的xml佈局包括了含有R.id.loadingView,R.id.contentView和R.id.errorView的view。
示例
接下來要舉的例子Github上也有中,我們使用CountriesAsyncLoader加載一系列的Country,並將其顯示在Fragment的RecyclerView中。大家可以從這個鏈接 https://db.tt/ycrCwt1L下載。
首先我們要定義CountriesView這一view interface 。
<code class="hljs php has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">interface</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesView</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpLceView</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">List</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Country</span>>>{</span> }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>
爲什麼要爲View定義接口呢?
1.因爲定義了這個接口之後我們可以更改view的實現。我們可以簡單地把代碼從一個繼承自 Activity的實現轉移到繼承自 Fragment的實現。
2.模塊性:我們可以移動獨立的庫項目中的整個業務邏輯層、Presenter以及View 接口,然後把這個包含了Presenter的庫應用到各類app當中。下圖中左側是使用了嵌入在ViewPager中的Activity的kicker app,以及使用嵌入在ViewPager中的Fragment的meinVerein app,如圖1-5。 兩者採用的是同一個定義了View接口和Presenter且測試了單元的庫。
由於我們可以通過執行view接口來模擬view,所以我們可以很容易地編寫單元測試。還有一個更簡單的方法就是在presenter中引入java接口並使用模擬presenter對象來編寫單元測試。
還有一個良性副作用就是,定義了view接口之後,我們不用直接從presenter再回調activity/fragment方法。我們這樣區分開來是因爲在執行presenter時我們在IDE自動完成上看到的方法只是關於view接口的方法。就我個人體會來說,我覺得這個方法非常有用,特別是團隊一起工作的時候。需要注意的是,除了定義一個CountriesView接口之外,我們還可以採用MvpLceView
<code class="language-xml hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">FrameLayoutxmlns:android="http:</span>//<span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">schemas.android.com</span>/<span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">apk</span>/<span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">res</span>/<span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android</span>" <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_width</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"match_parent"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_height</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"match_parent"</span> ></span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"><!-- Loading View --></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">ProgressBar </span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:id</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"@+id/loadingView"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_width</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"wrap_content"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_height</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"wrap_content"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_gravity</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"center"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:indeterminate</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"true"</span> /></span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"><!-- Content View --></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">android.support.v4.widget.SwipeRefreshLayout </span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:id</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"@+id/contentView"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_width</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"match_parent"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_height</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"match_parent"</span> ></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">android.support.v7.widget.RecyclerView </span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:id</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"@+id/recyclerView"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_width</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"match_parent"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_height</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"match_parent"</span> /></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">android.support.v4.widget.SwipeRefreshLayout</span>></span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"><!-- Error view --></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">TextView </span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:id</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"@+id/errorView"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_width</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"wrap_content"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">android:layout_height</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"wrap_content"</span> /></span> <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">FrameLayout</span>></span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li></ul>
CountriesPresenter控制CountriesView並運行CountriesAsyncLoader。
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesPresenter</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpBasePresenter</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesView</span>>{</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">loadCountries</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> pullToRefresh){ getView().showLoading(pullToRefresh); CountriesAsyncLoader countriesLoader = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> CountriesAsyncLoader( <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> CountriesAsyncLoader.CountriesLoaderListener(){ <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onSuccess</span>(List<Country> countries){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span>(isViewAttached()){ getView().setData(countries); getView().showContent(); } } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onError</span>(Exception e){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span>(isViewAttached()){ getView().showError(e,pullToRefresh); } } }); countriesLoader.execute(); } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li></ul>
實現CountriesView接口 的CountriesFragment 如下所示:
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesFragment</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpLceFragment</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">SwipeRefreshLayout</span>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">List</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Country</span>>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesView</span>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesPresenter</span>> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesView</span>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">SwipeRefreshLayout</span>.<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">OnRefreshListener</span>{</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@InjectView</span>(R.id.recyclerView)RecyclerViewrecyclerView; CountriesAdapteradapter; <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onViewCreated</span>(View view,@Nullable Bundle savedInstance){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onViewCreated(view,savedInstance); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Setup contentView == SwipeRefreshView</span> contentView.setOnRefreshListener(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Setup recycler view</span> adapter = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> CountriesAdapter(getActivity()); recyclerView.setLayoutManager(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> LinearLayoutManager(getActivity())); recyclerView.setAdapter(adapter); loadData(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">false</span>); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">loadData</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> pullToRefresh){ presenter.loadCountries(pullToRefresh); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> CountriesPresenter <span class="hljs-title" style="box-sizing: border-box;">createPresenter</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> SimpleCountriesPresenter(); } <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Just a shorthand that will be called in onCreateView()</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> <span class="hljs-title" style="box-sizing: border-box;">getLayoutRes</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> R.layout.countries_list; } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">setData</span>(List<Country> data){ adapter.setCountries(data); adapter.notifyDataSetChanged(); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onRefresh</span>(){ loadData(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>); } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li></ul>
代碼數量也並不是很多嘛,對吧?這是因爲基類已經執行了從加載view到content view或error view的轉換。我們可能第一眼看到那一列MvpLceFragment類屬參數會覺得灰心。但是我要解釋一下:第一種類屬參數代表的是content view的類型;第二種是指以fragment顯示的Model;第三種是View接口;最後一種是Presenter的類型。總結起來就是:MvpLceFragment
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> View <span class="hljs-title" style="box-sizing: border-box;">onCreateView</span>(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState){ Return inflater.inflate(getLayoutRes(),container,<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">false</span>); }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>
因此,我們不用重寫onCreateView(),只需重寫getLayoutRes()。一般來說,onCreateView()只能創建view而onViewCreated()需要被重寫,以便爲RecyclerView初始化Adapter等項。因此,千萬不要忘記調用super.OnViewCreated();
ViewState模塊
看到這裏大家應該大概瞭解瞭如何運用Mosby庫。Mosby中的ViewState模塊能幫助我們在Android開發中解決一些棘手的難題:處理屏幕旋轉。
問:如果把正在運行country這個例子的app並顯示了一列country的設備從橫屏旋轉到豎屏,會出現什麼情況?
答:大家到這個視頻鏈接https://youtu.be/9iSBGEIZmUw中看看,結果是一個新的 CountriesFragment會被實例化,app開始顯示ProgressBar(並重新加載country列表)而不再在RecyclerView中顯示country列表(屏幕旋轉前的狀態)
Mosby引入了ViewState來解決這個問題。原理就是,我們跟蹤presenter從關聯的View中調用的方法。比如,presenter調用的是view.showContent(),一旦showContent()被調用,view就會意識到其狀態變更爲“showing content”,從而view把這一信息存儲到一個ViewState。如果view在方向改變過程中遭到破壞,那麼ViewState 就會被存儲到Activity.onSaveInstanceState(Bundle) 或 Fragment.onSaveInstanceState(Bundle)中,並在Activity.onCreate(Bundle) 或Fragment.onActivityCreated(Bundle)中修復。
由於不是每種數據都能存儲在Bundle中,所以不同的數據類型採用不同的ViewState 實現:數據類型ArrayList採用ArrayListLceViewState;數據類型Parcelable 採用Parcelable DataLceViewState;數據類型Serializeable採用SerializeableLceViewState。如果使用的是一個可保持( Retaining )的Fragment,那麼 ViewState在屏幕旋轉時不會被破壞,所以也就不需要存儲到Bundle中。因此,它可以存儲任何類型的數據。在這種情況下,我們需要使用RetainingFragmentLceViewState。存儲一個ViewState比較容易。由於我們的架構比較整潔,我們的View又有接口,ViewState 可以向presenter一樣通過調用同樣的接口方法來修複相關聯的view。舉個例子,MvpLceView一般有3種狀態,即:顯示showContent(),showLoading()和showError(),所以ViewState本身會調用相應的方法來修復view的狀態。
那只是一些內部構件。如果大家想編寫自定義的ViewState,瞭解以上內容就夠了。ViewStates的使用非常簡單。事實上,要把MvpLceFragment 遷移到MvpLceViewStateFragment ,我們只需要另外執行createViewState() 和 getData()。下面我們就在CountriesFragment中實踐一下吧:
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesFragment</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpLceViewStateFragment</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">SwipeRefreshLayout</span>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">List</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Country</span>>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesView</span>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesPresenter</span>> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CountriesView</span>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">SwipeRefreshLayout</span>.<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">OnRefreshListener</span>{</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@InjectView</span>(R.id.recyclerView)RecyclerView recyclerView; CountriesAdapter adapter; <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> LceViewState<List<Country>,CountriesView> <span class="hljs-title" style="box-sizing: border-box;">createViewState</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> RetainingFragmentLceViewState<List<Country>,CountriesView>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> List<Country> <span class="hljs-title" style="box-sizing: border-box;">getData</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> adapter == <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>? <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span> : adapter.getCountries(); } <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// The code below is the same as before</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onViewCreated</span>(Viewview,@Nullable Bundle savedInstance){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onViewCreated(view,savedInstance); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Setup contentView == SwipeRefreshView</span> contentView.setOnRefreshListener(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Setup recycler view</span> adapter = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> CountriesAdapter(getActivity()); recyclerView.setLayoutManager(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> LinearLayoutManager(getActivity())); recyclerView.setAdapter(adapter); loadData(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">false</span>); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">loadData</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> pullToRefresh){ presenter.loadCountries(pullToRefresh); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> CountriesPresenter <span class="hljs-title" style="box-sizing: border-box;">createPresenter</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> SimpleCountriesPresenter(); } <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Just a shorthand that will be called in onCreateView()</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> <span class="hljs-title" style="box-sizing: border-box;">getLayoutRes</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> R.layout.countries_list; } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">setData</span>(List<Country> data){ adapter.setCountries(data); adapter.notifyDataSetChanged(); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onRefresh</span>(){ loadData(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>); } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li><li style="box-sizing: border-box; padding: 0px 5px;">46</li><li style="box-sizing: border-box; padding: 0px 5px;">47</li><li style="box-sizing: border-box; padding: 0px 5px;">48</li><li style="box-sizing: border-box; padding: 0px 5px;">49</li><li style="box-sizing: border-box; padding: 0px 5px;">50</li><li style="box-sizing: border-box; padding: 0px 5px;">51</li><li style="box-sizing: border-box; padding: 0px 5px;">52</li><li style="box-sizing: border-box; padding: 0px 5px;">53</li></ul>
以上就是全部過程啦。我們不必更改presenter或其他代碼。這裏 是一個關於我們的獲得ViewState支持的CountriesFragment的視頻。在這個視頻中我們可以看到,view在方位轉變之後仍然處於同樣的“狀態”,即,view橫屏顯示country列表,隨後橫屏顯示country列表。View能橫屏顯示下拉刷新指示,變更爲豎屏時也能顯示。
自定義ViewState
ViewState確實是一個強大且靈活的概念。看到這裏我相信大家都瞭解了LCE (Loading-Content-Error) ViewState的易用性。下面我們就一起來編寫自己的View和ViewState吧。我們的View只顯示兩類不同的數據對象:A和B。結果應該像這個視頻 https://youtu.be/9iSBGEIZmUw 中演示的這樣:
大家心裏肯定覺得,這也不怎麼樣啊!別介啊,我只是想演示一下創建自己的ViewState是一件多麼容易的事。
View 接口和數據對象(model)如下所示:
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">A</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Parcelable</span> {</span> String name; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-title" style="box-sizing: border-box;">A</span>(String name){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.name=name; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> String <span class="hljs-title" style="box-sizing: border-box;">getName</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> name; } } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">B</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Parcelable</span> {</span> String foo; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-title" style="box-sizing: border-box;">B</span>(String foo){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.foo=foo; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> String <span class="hljs-title" style="box-sizing: border-box;">getFoo</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> foo; } } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">interface</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyCustomView</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpView</span>{</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">showA</span>(A a); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">showB</span>(B b); }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li></ul>
在這個簡單的例子中我們沒有加入業務邏輯層。因爲我們假設在實際的app中如果有業務邏輯層的話會使整個生成A或B的操作變得複雜。Presenter如下所示:
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyCustomPresenter</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpBasePresenter</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyCustomView</span>>{</span> Random random = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> Random(); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">doA</span>(){ A a = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> A(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"My name is A "</span>+random.nextInt(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>)); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span>(isViewAttached()){ getView().showA(a); } } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">doB</span>(){ B b = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> B(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"I am B "</span>+random.nextInt(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>)); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span>(isViewAttached()){ getView().showB(b); } } } </code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li></ul>
我們定義了實現了MyCustomView接口的MyCustomActivity。
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyCustomActivity</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MvpViewStateActivity</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyCustomPresenter</span>> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyCustomView</span>{</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@InjectView</span>(R.id.textViewA) TextViewaView; <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@InjectView</span>(R.id.textViewB) TextViewbView; <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onCreate</span>(Bundle savedInstanceState){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onCreate(savedInstanceState); setContentView(R.layout.my_custom_view); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> RestoreableViewState <span class="hljs-title" style="box-sizing: border-box;">createViewState</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> MyCustomViewState();<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Our ViewState implementation</span> } <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Will be called when no view state exist yet,</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// which is the case the first time MyCustomActivity starts</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> onNew <span class="hljs-title" style="box-sizing: border-box;">ViewStateInstance</span>(){ presenter.doA(); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> MyCustomPresenter <span class="hljs-title" style="box-sizing: border-box;">createPresenter</span>(){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> MyCustomPresenter(); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">showA</span>(A a){ MyCustomViewState vs = ((MyCustomViewState)viewState); vs.setShowingA(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>); vs.setData(a); aView.setText(a.getName()); aView.setVisibility(View.VISIBLE); bView.setVisibility(View.GONE); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">showB</span>(B b){ MyCustomViewState vs=((MyCustomViewState)viewState); vs.setShowingA(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">false</span>); vs.setData(b); bView.setText(b.getFoo()); aView.setVisibility(View.GONE); bView.setVisibility(View.VISIBLE); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@OnClick</span>(R.id.loadA)<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onLoadAClicked</span>(){ presenter.doA(); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@OnClick</span>(R.id.loadB)<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onLoadBClicked</span>(){ presenter.doB(); } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li><li style="box-sizing: border-box; padding: 0px 5px;">46</li><li style="box-sizing: border-box; padding: 0px 5px;">47</li><li style="box-sizing: border-box; padding: 0px 5px;">48</li><li style="box-sizing: border-box; padding: 0px 5px;">49</li><li style="box-sizing: border-box; padding: 0px 5px;">50</li><li style="box-sizing: border-box; padding: 0px 5px;">51</li></ul>
由於我們沒有LCE(Loading-Content-Error),所以不把 MvpLceActivity作爲基類。我們採用的是最普遍的支持 ViewState的MvpViewStateActivity作爲基類。基本上我們的View只顯示aView 或 bView。
在onNew ViewStateInstance()中,我們需要明確在第一個Activity運行時需要做什麼,因爲先前並不存在ViewState 例子用於修復。在showA(A a) 和 showB(B b)中,我們需要將顯示A 或 B的信息存儲到ViewState。到這一步,我們就差不多完成了,現在只差MyCustomViewState執行這一步啦:
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">ublic class MyCustomViewState implements RestoreableViewState<MyCustomView>{ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> String KEY_STATE=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"MyCustomViewState-flag"</span>; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> String KEY_DATA=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"MyCustomViewState-data"</span>; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> showingA=<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>;<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// if false, then show B</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> Parcelable data;<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Can be A or B</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">saveInstanceState</span>(Bundle out){ out.putBoolean (KEY_STATE,showingA); out.putParcelable (KEY_DATA,data); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> <span class="hljs-title" style="box-sizing: border-box;">restoreInstanceState</span>(Bundle in){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span>(in==<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">false</span>; } showingA = in.getBoolean (KEY_STATE,<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>); data = in.getParcelable (KEY_DATA); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>; } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">apply</span>(MyCustomView view,<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> retained){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span>(showingA){ view.showA((A)data); }<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span>{ view.showB((B)data); } } <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @param</span> a true if showing a, false if showing b */</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">setShowingA</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> a){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.showingA=a; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">setData</span>(Parcelable data){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.data=data; } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li></ul>
大家可以看到,我們需要把ViewState保存到從Activity.onSaveInstanceState()調用的 saveInstanceState()中,並且在從Activity.onCreate()調用的restoreInstanceState()中修復viewstate的數據。apply()方法將會從Activity中調用以修復view state。我們像presenter一樣通過調用同樣的View interface 方法showA() 或 showB()來實現這一操作。
大家可以看到,我們需要把ViewState保存到從Activity.onSaveInstanceState()調用的 saveInstanceState()中,並且在從Activity.onCreate()調用的restoreInstanceState()中修復viewstate的數據。apply()方法將會從Activity中調用以修復view state。我們像presenter一樣通過調用同樣的View interface 方法showA() 或 showB()來實現這一操作。
這個外部的ViewState把view state修復的複雜性和職責從Activity代碼中剝離,併入到這個單獨的類中。而編寫ViewState類的單元測試要比Activity類的單元測試容易得多。
怎樣處理後臺線程?
通常,Presenter會管理後臺線程。Presenter如何處理後臺線程取決於它所關聯的Activity或者Fragment ,具體分爲兩種情況:
- 可保持的Fragment : 如果你調用了Fragment的setRetainInstanceState(true)那麼這個Fragment在屏幕旋轉時就不會被銷燬。只有該Fragment的GUI會被銷燬,並且在屏幕旋轉時重新調用onCreateView創建視圖。這就是說當屏幕旋轉時Fragment所有的成員成員變量和Presenter不會發生變化。在這個示例中,我們將新的視圖關聯到Presenter中。因此,Presenter不需要去掉任何正在運行中的後臺任務,因爲Presenter已經關聯了新的視圖。例如:
1.豎屏情況下啓動應用
2.實例化Fragment時會調用onCreate()、onCreateView()、createPresenter(), 然後通過調用presenter的attachView()函數將View關聯到Presenter中。
3. 下一步我們旋轉手機屏幕,從豎屏切換到橫屏;
4. 此時onDestroyView() 會調用,而onDestroyView() 又會調用presenter的detachView(true)函數。我們注意到detachView有個參數爲true,這是告訴presenter這個Fragment是可持有的Fragment(否則這個參數應該爲false)。通過這個參數,presenter就知道它不需要取消正在運行的後臺任務;
5. 應用現在是橫屏狀態了,在旋轉時onCreateView方法會被調用,但是createPresenter()函數不會被調用,因爲我們會對presenter 進行不爲空的判斷,當presenter爲空時才調用createPresenter()函數。而Fragment的setRetainInstanceState(true)會保持這個presenter對象,因此presenter此時不會被重新創建;
6. 在調用了presenter的attachView()之後新創建的View會被重新關聯到presenter中。
7. ViewState會被恢復,但是沒有後臺任務會被取消,因此也沒有後臺任務需要重新啓動。
- Activity和不保持的Fragment :在這個示例中工作流非常的簡單。所有的東西都會被銷燬,包括presenter。因此presenter對象應該取消所有正在運行的任務。例如 :
我們採用非保持fragment在豎屏情況下啓動app。
8.我們採用非保持fragment在豎屏情況下啓動app。
9.Fragment被實例化之後,調用onCreate(), onCreateView(),和createPresenter(),然後通過調用presenter.attachView()將view(fragment)附着到presenter。
10.下一步我們旋轉設備屏幕,從豎屏切換到橫屏。
11.此時onDestroyView() 會調用,而onDestroyView() 又會調用presenter的detachView(true)函數。Presenter取消後臺任務。
12. onSaveInstanceState(Bundle)被調用, ViewState被保存到Bundle中。
13. App現在出於橫屏狀態。新的Fragment被實例化並調用onCreate(),onCreateView()和 createPresenter()來創建一個新的presenter例子,通過調用presenter.attachView()將新的view附着到新的presenter
14. ViewState會從Bundle中恢復,且view的狀態也會被恢復。如果ViewState是showLoading,那麼presenter會重新啓動後臺線程來加載數據。
15. 以下是獲得ViewState支持的Activity的生命週期圖解,如圖1-6:
以下是獲得ViewState支持的Fragment的生命週期圖解, 如圖 1-7:
Retrofit模塊
Mosby提供了 LceRetrofitPresenter 和 LceCallback。爲獲得LCE方法showLoading(), showContent() 和 showError()支持的Retrofit編寫presenter ,幾行代碼就能搞定。
<code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MembersPresenter</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">LceRetrofitPresenter</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MembersView</span>,<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">List</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">User</span>>>{</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> GithubApigithubApi; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-title" style="box-sizing: border-box;">MembersPresenter</span>(GithubApi githubApi){ <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.githubApi=githubApi; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">loadSquareMembers</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">boolean</span> pullToRefresh){ githubApi.getMembers(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"square"</span>,<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> LceCallback(pullToRefresh)); } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li></ul>
Dagger模塊
想在不依靠注入式的情況下寫應用?Ted Mosby告訴你,這是行不通滴!Dagger是java依賴注入式框架最常用的方法,也是Android開發者們的心頭好。Mosby支持Dagger1。Mosby通過一個叫做getObjectGraph()的方法提供Injector界面。通常,我們的應用模塊非常廣泛。要想輕鬆分享這一模塊,我們需要把android.app.Application歸入子類,使其執行Injector。之後所有的Activity和Fragment都可以通過調用getObjectGraph()來存取ObjectGraph,因爲DaggerActivity and DaggerFragment也都是Injector。我們也可以通過重寫Activity 或 Fragment中的 getObjcetGraph() ,從而調用plus(Module)以增加模塊。我個人已經用到Dagger2了,它與Mosby也兼容。大家可以在Github上找到關於Dagger1 和 Dagger2的示例。點此這個鏈接https://db.tt/3fVqVdAzDagger1示例 apk;點此這個鏈接https://db.tt/z85y4fSYDagger2 示例 apk。
Rx模塊
Observables贊爆了!現在稍微潮一點的小夥兒們都用RxJava了好嗎!你猜結果怎麼着?RxJava確實是太酷了!所以,Mosby給大家提供一個本質上是Subscriber的MvpLceRxPresenter,它能幫我們自動處理onNext(), onCompleted() 和 onError()並回調相應的LCE方法,比如showLoading(), shwoContent() 和 showError()。它還將 RxAndroid 附帶到observerOn() Android主要 UI 線程。你可能覺得,要是用了RxJava的話就不再需要Model View Presenter了。呃,那只是你的一家之言。在我看來,把View和Model清晰地區分開來非常重要。而且我也認爲其中的某些好用的功能在沒有MVP的情況下不容易執行。最後,大家要是還想回到過去那個Activity和Fragment包含了上千條又臭又長的代碼行時代,那麼我祝你在麪條式代碼的地獄裏過得愉快。好了,廢話不多說,我介紹的方法不屬於麪條式代碼是因爲Observerables引入了一個結構齊整的工作流,把Activity或Fragment做成一個BLOB的想法已經近在咫尺了。
測試模塊
大家可能注意到這裏存在着一個測試模塊。這個模塊用於Mosby庫的內部測試。但是,它也可以爲我們自己的app所用。它使用Robolectric爲我們的LCE Presenter, Activities 和 Fragments提供單元測試模板。它的基本功能是查看測試中的Presenter是否正確工作:通過觀察presenter時候調用showLoading(),
showContent() 和 showError()。我們還可以驗證setData()中的數據。所以我們可以爲Presenter和底層編寫類似黑匣子的測試。Mosby的測試模塊也提供了測試MvpLceFragment 或 MvpLceActivity的可能性。它相當於一種“精簡版”的UI 測試。這些測試通過查看xml佈局是否包含R.id.loadingView, R.id.contentView
和R.id.errorView之類的指定id、loadingView是否可視,在加載view時,是否是錯誤的view可視、content view能否處理由setData()提交的已加載數據等方面來檢驗Fragment或Activity是否正常工作,是否遇到crashing。它和Espresso類的UI測試並不相同。我覺得沒有必要爲LCE View單獨寫一個UI 測試。
以下是Ted Mosby庫的一些測試小建議:
1. 編寫傳統的單元測試來測試業務邏輯層和model。
2. 使用MvpLcePresenterTest來測試presenter。
3.使用MvpLceFragmentTest 和 MvpLceActivityTest來測試MvpLceFragment 和 Activity。
4.如果有必要,可以使用Espresso來編寫UI測試。
測試模塊尚未完成。大家可以看到這個模塊是測試版,因爲Robolectric 3.0還沒完成,而且Android gradle plugin也沒用完全支持傳統的單元測試。android gradle plugin
1.2應該會好得多。Robolectric 和 androids gradle plugin可以用了之後我會再寫一篇關於Mosby,Dagger,Retrofit和RxJava單元測試的博客。