Model-View-Presenter:Android指南
網上有很多關于MVP架構的文章和示例,并且有很多不同的實現。但開發者社區仍不斷努力,想以盡可能最好的方式將此模式應用在Android上。
如果你決定采用這種模式,你正在做一個架構選擇,你必須知道你的代碼庫將改變,以及你新的功能也要用新的方法來開發。另外你需要面對常見的Android問題如Activity生命周期,然后你還應該問問自己下面這些問題:
- 我應該保存 presenter 的狀態嗎?
- 我應該將 presenter 做持久化處理嗎?
- presenter 需要有生命周期嗎?
在本文中,我將提供一系列準則或最佳做法,以便:
- 解決采用這個架構遇到的最常見問題(至少是一些我遇到過的)
- 發揮這個架構的最大優勢
首先,讓我們先解釋一下這個模式:
- Model :它是負責管理數據的接口。模型的職責包括使用API,緩存數據,管理數據庫等。該模型還可以是與負責這些職責的其他模塊通信的接口。例如,如果你使用Repository模式,則模型可以是Repository。如果你使用的是Clean架構,那么Model可以是一個Interactor。
- Presenter :presenter是model和view的中間人。你的所有業務邏輯都應該放在這里面。presenter負責查詢model和更新view,對更新模型的用戶交互作出反應。
- View :它只負責以presenter定義的方式來顯示數據。view可以被Activities、 Fragments、任何Android widget或者其他一些像顯示ProgressBar、更新TextView、填充RecyclerView等等可執行操作的視圖。
下面是以我的觀點列出的一些指南,你可能不會全部贊同,不過我會試著解釋為什么這么做。
1. 讓View變得被動和無知
Android中最大的一個問題就是view(Activities、Fragments等)不是那么容易被測試因為Android框架很復雜。為了解決這個問題,你需要實現 Passive View 模式。這種實現方式通過利用一個controller來減少view的業務行為,在我們的例子中,這個controller是presenter。這種方式顯著的提高的代碼的可測試性。
例如,如果你有一個username/password的表單和一個提交按鈕,你不需要在view中寫驗證邏輯而是將它寫在presenter中。你的view只管接受用戶名和密碼的輸入然后將他們傳遞給presenter即可。
2. 使presenter與框架無關
為了提高代碼的可測試性,那么就要確保presenter不能依賴Android類文件。presenter用純java代碼實現的兩個理由:首先你要將具體的實現抽象到presenter中,這樣的話你就可以寫不依賴于設備的測試代碼了,可以快速的在你的本地JVM中運行而不需要模擬器。
如果我需要用到Context呢?
那么就不要用它。在這種情況下,你應該問一下自己為什么需要context呢。我猜你可能想要存儲數據或者獲取資源。但是你不需要在presenter中做這些:你可以在view中獲取資源,在model中存儲數據。這里只是兩個簡單的例子,不過我敢打賭大多數情況下都是因為類的職責不明確導致的。
順便說一下,依賴倒置原則可以幫助你在這種情況下解耦。
3. 寫一個contract類來描述View和Presenter之間的交互
當你準備開始寫一個新功能時,第一步最好先寫一個contract類。contract描述了view和presenter之間的交互,它幫助你以更干凈的方式設計交互。
我喜歡用Google在 Android Architecture repository中建議的解決方案:這個contract接口類中包含兩個接口一個是view另一個是presenter。
讓我們舉個例子。
public interface SearchRepositoriesContract {
interface View {
void addResults(List<Repository> repos);
void clearResults();
void showContentLoading();
void hideContentLoading();
void showListLoading();
void hideListLoading();
void showContentError();
void hideContentError();
void showListError();
void showEmptyResultsView();
void hideEmptyResultsView();
}
interface Presenter extends BasePresenter<View> {
void load();
void loadMore();
void queryChanged(String query);
void repositoryClick(Repository repo);
}
}
看到這個方法的名字,你應該就明白這個例子中的contract是干什么的了吧。
如果你還不知道,那一定是你的問題哈哈。
在這個例子中你可以看到view中定義的方法非常簡單而且不包含任何邏輯。
The View contract
正如我之前說過的,view接口是要被Activity或者Fragment實現的。presenter必須依賴于view接口而不是直接依賴于Activity:通過這種方式,你可以將presenter從視圖實現解耦,遵循SOLID原則的D:“依賴抽象,不要依賴具體實現)。
我們不需要更改presenter中的一行代碼就可以替換具體的視圖。因此我們可以非常容易的通過創建一個mock view來進行單元測試。
The presenter contract
等等,我們真的需要一個Presenter接口嗎?
事實上不需要,但我認為還是要的。
關于這個話題有兩種不同的思想流派。
一些人認為應該寫一個Presenter接口因為你要將具體的presenter和view解耦。
然而另外一些開發者認為你在抽象的東西已經是一個抽象的了所以不需要再寫一個接口了。另外不管怎么樣,有了一個接口后可以幫你方便的寫mock presenter,不過如果你采用了 Mockito 這樣的工具類那么你就不需要接口了。
我個人還是喜歡寫這么一個 Presenter 接口的,下面是兩個簡單的理由:
- 我不是去為presenter寫一個接口而是寫一個Contract類來描述view和presenter之間的交互。
- 寫這么個接口并不費什么力。
我已經這么寫超過一年了甚至更長,至今沒有發現什么問題。
4. 定義一個名稱方便區分責任
presenter通常有兩種類型的方法:
- Actions (e.g: load()):presenter的一些行為操作。
- User events (e.g:queryChanged(…)):用戶觸發的操作比如在搜索框中鍵入字符或者是點擊列表中的某個選項。
你定義的action越多那么view中的邏輯也就越多。
當用戶滾動到列表的結尾時將調用loadMore()方法,然后presenter加載另外一頁的結果。這意味著當用戶滾動到結尾時,view知道必須加載新頁面。我可以命名方法onScrolledToEnd()讓具體的presenter處理具體做什么。
我想說的是,在“contract設計”階段,你必須定義好每個用戶事件,相應的action是什么,邏輯應該屬于誰。
5. 不要在Presenter接口中創建Activity-lifecycle-style回調
我使用這個標題的意思是presenter不應該有像onCreate(…),onStart(),onResume()等方法原因如下:
- 如果這么做了的話presenter將會和Activity產生耦合。如果我想用一個Fragment替換Activity怎么辦?我什么時候應該調用presenter.onCreate(state)方法?在fragment的onCreate(…)、onCreateView(…)還是onViewCreated(…)中?如果我使用自定義view怎么辦?
- presenter不應該有這么復雜的生命周期。事實上,主要的Android組件都是以這種方式設計的,但并不意味著你必須也這么做。如果你有機會可以簡化的話那就簡化它吧。
6. Presenter和view有1對1的關系
如果沒有view的話presenter就沒有意義了。presenter隨著view一起被創建也隨著view一起被銷毀。一個presenter管理一個view。
你可以通過多種方式處理presenter中view的依賴。一種方式是在presenter接口中提供像attach(View view)和detach()的方法就像之前例子中展示的那樣。不過這樣做有一個問題就是你需要注意view是否為null,每次presenter用到它的時候都要檢查一下是否為null。這點確實有點煩……
我說了presenter和view是一對一的關系。我們可以利用這一點,實際上具體的presenter可以將view實例作為構造函數的參數傳入。順便說一句,你可能需要一個方法來訂閱presenter的一些事件。所以我建議定義一個方法start()(或類似的方法)來運行Presenter中的業務。
關于 detach() 呢?
如果你有一個叫start()的方法,那么你可能至少還需要一個來釋放依賴的方法。既然我們定義訂閱presenter一些事件的方法叫start(),那么另一個方法就叫stop()吧。
public interface BasePresenter<V> {
void attach(V view);
void detach();
}
public interface BasePresesnter {
void start();
void stop();
}
7. 不要在presenter中保存狀態
我的想著是要用Bundle來保存。但考慮到上面的第二條準則就不能這么做了。你不能將數據序列化到Bundle中,因為這樣的話presenter就與Android類耦合了。
我說presenter應該是無狀態的,但其實也不然。在我之前描述的例子中,presenter應該至少具有頁碼/偏移量之類的狀態。
8. 不要持久化presenter
我不喜歡這種方式主要是因為我認為presenter不是我們應該持久化的,要清楚它不是一個數據類。
一些建議提供了一種在配置發生改變的時候通過恢復fragments或者 Loaders 的方式記住presenter的狀態。我不認為這是最好的解決方案。通過這種方式presenter可以在方向發生變化恢復,但是當Android殺死了進程并銷毀Activity,后者將與新的presenter一起重新創建。因此,該解決方案僅解決了一半的問題。
9. 為Model提供緩存以恢復視圖狀態
在我看來,解決“恢復狀態”問題需要一些應用架構的知識。基本上,作者建議使用類似Repository或任何旨在管理數據的接口來緩存網絡結果,范圍限定于應用程序而不是Activity。
這個接口只是一個更聰明的Model。后者應至少提供磁盤緩存策略和可能的內存緩存。這樣的話,即使進程被殺,presenter也可以使用磁盤緩存恢復視圖狀態。
view應該只關心必要的請求參數以恢復狀態。例如,在我們的示例中,我們只需要保存查詢。
現在,你有兩個選擇:
- 你在model層中抽象這個行為,當presenter調用repository.get(params)時,如果頁面已經在緩存中,數據源只返回它,否則再調用API。
- 在contract中的presenter添加一個方法來恢復視圖狀態。restore(params),loadFromCache(params)或reload(params)這些是描述相同動作的不同名稱你可以隨便選一個。
來自:http://mafei.me/2017/03/10/Model-View-Presenter:Android指南/