原文地址:https://medium.com/@cervonefrancesco/model-view-presenter-android-guidelines-94970b430ddf#.nqgbpr2bj
網(wǎng)上有很多關(guān)于MVP架構(gòu)的文章和示例,并且有很多不同的實(shí)現(xiàn)。但開發(fā)者社區(qū)仍不斷努力,想以盡可能最好的方式將此模式應(yīng)用在Android上馆类。
如果你決定采用這種模式架诞,你正在做一個(gè)架構(gòu)選擇缀去,你必須知道你的代碼庫(kù)將改變软舌,以及你新的功能也要用新的方法來開發(fā)羽圃。另外你需要面對(duì)常見的Android問題如Activity生命周期欢搜,然后你還應(yīng)該問問自己下面這些問題:
- 我應(yīng)該保存presenter的狀態(tài)嗎封豪?
- 我應(yīng)該將presenter做持久化處理嗎?
- presenter需要有生命周期嗎炒瘟?
在本文中撑毛,我將提供一系列準(zhǔn)則或最佳做法,以便:
- 解決采用這個(gè)架構(gòu)遇到的最常見問題(至少是一些我遇到過的)
- 發(fā)揮這個(gè)架構(gòu)的最大優(yōu)勢(shì)
首先唧领,讓我們先解釋一下這個(gè)模式:
- Model:它是負(fù)責(zé)管理數(shù)據(jù)的接口藻雌。模型的職責(zé)包括使用API,緩存數(shù)據(jù)斩个,管理數(shù)據(jù)庫(kù)等胯杭。該模型還可以是與負(fù)責(zé)這些職責(zé)的其他模塊通信的接口。例如受啥,如果你使用Repository模式做个,則模型可以是Repository鸽心。如果你使用的是Clean架構(gòu),那么Model可以是一個(gè)Interactor居暖。
- Presenter:presenter是model和view的中間人顽频。你的所有業(yè)務(wù)邏輯都應(yīng)該放在這里面。presenter負(fù)責(zé)查詢model和更新view太闺,對(duì)更新模型的用戶交互作出反應(yīng)糯景。
- View:它只負(fù)責(zé)以presenter定義的方式來顯示數(shù)據(jù)。view可以被Activities省骂、 Fragments蟀淮、任何Android widget或者其他一些像顯示ProgressBar、更新TextView钞澳、填充RecyclerView等等可執(zhí)行操作的視圖怠惶。
下面是以我的觀點(diǎn)列出的一些指南,你可能不會(huì)全部贊同轧粟,不過我會(huì)試著解釋為什么這么做策治。
1. 讓View變得被動(dòng)和無知
Android中最大的一個(gè)問題就是view(Activities、Fragments等)不是那么容易被測(cè)試因?yàn)锳ndroid框架很復(fù)雜兰吟。為了解決這個(gè)問題通惫,你需要實(shí)現(xiàn)Passive View模式。這種實(shí)現(xiàn)方式通過利用一個(gè)controller來減少view的業(yè)務(wù)行為揽祥,在我們的例子中,這個(gè)controller是presenter檩电。這種方式顯著的提高的代碼的可測(cè)試性拄丰。
例如,如果你有一個(gè)username/password的表單和一個(gè)提交按鈕俐末,你不需要在view中寫驗(yàn)證邏輯而是將它寫在presenter中料按。你的view只管接受用戶名和密碼的輸入然后將他們傳遞給presenter即可。
2. 使presenter與框架無關(guān)
為了提高代碼的可測(cè)試性卓箫,那么就要確保presenter不能依賴Android類文件载矿。presenter用純java代碼實(shí)現(xiàn)的兩個(gè)理由:首先你要將具體的實(shí)現(xiàn)抽象到presenter中,這樣的話你就可以寫不依賴于設(shè)備的測(cè)試代碼了(甚至都不需要Robolectric)烹卒,可以快速的在你的本地JVM中運(yùn)行而不需要模擬器闷盔。
如果我需要用到Context呢?
那么就不要用它。在這種情況下旅急,你應(yīng)該問一下自己為什么需要context呢逢勾。我猜你可能想要存儲(chǔ)數(shù)據(jù)或者獲取資源。但是你不需要在presenter中做這些:你可以在view中獲取資源藐吮,在model中存儲(chǔ)數(shù)據(jù)溺拱。這里只是兩個(gè)簡(jiǎn)單的例子逃贝,不過我敢打賭大多數(shù)情況下都是因?yàn)轭惖穆氊?zé)不明確導(dǎo)致的。
順便說一下迫摔,依賴倒置原則可以幫助你在這種情況下解耦沐扳。
3. 寫一個(gè)contract類來描述View和Presenter之間的交互
當(dāng)你準(zhǔn)備開始寫一個(gè)新功能時(shí),第一步最好先寫一個(gè)contract類句占。contract描述了view和presenter之間的交互沪摄,它幫助你以更干凈的方式設(shè)計(jì)交互。
我喜歡用Google在 Android Architecture repository中建議的解決方案:這個(gè)contract接口類中包含兩個(gè)接口一個(gè)是view另一個(gè)是presenter辖众。
讓我們舉個(gè)例子卓起。
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);
}
}
看到這個(gè)方法的名字,你應(yīng)該就明白這個(gè)例子中的contract是干什么的了吧凹炸。
如果你還不知道戏阅,那一定是你的問題哈哈。
在這個(gè)例子中你可以看到view中定義的方法非常簡(jiǎn)單而且不包含任何邏輯啤它。
The View contract
正如我之前說過的奕筐,view接口是要被Activity或者Fragment實(shí)現(xiàn)的。presenter必須依賴于view接口而不是直接依賴于Activity:通過這種方式变骡,你可以將presenter從視圖實(shí)現(xiàn)解耦离赫,遵循SOLID原則的D:“依賴抽象,不要依賴具體實(shí)現(xiàn))塌碌。
我們不需要更改presenter中的一行代碼就可以替換具體的視圖渊胸。因此我們可以非常容易的通過創(chuàng)建一個(gè)mock view來進(jìn)行單元測(cè)試。
The presenter contract
等等台妆,我們真的需要一個(gè)Presenter接口嗎翎猛?
事實(shí)上不需要,但我認(rèn)為還是要的接剩。
關(guān)于這個(gè)話題有兩種不同的思想流派切厘。
一些人認(rèn)為應(yīng)該寫一個(gè)Presenter接口因?yàn)槟阋獙⒕唧w的presenter和view解耦。
然而另外一些開發(fā)者認(rèn)為你在抽象的東西已經(jīng)是一個(gè)抽象的了所以不需要再寫一個(gè)接口了懊缺。另外不管怎么樣疫稿,有了一個(gè)接口后可以幫你方便的寫mock presenter,不過如果你采用了Mockito這樣的工具類那么你就不需要接口了鹃两。
我個(gè)人還是喜歡寫這么一個(gè)Presenter接口的遗座,下面是兩個(gè)簡(jiǎn)單的理由:
- 我不是去為presenter寫一個(gè)接口而是寫一個(gè)Contract類來描述view和presenter之間的交互。
- 寫這么個(gè)接口并不費(fèi)什么力俊扳。
我已經(jīng)這么寫超過一年了甚至更長(zhǎng)员萍,至今沒有發(fā)現(xiàn)什么問題。
4. 定義一個(gè)名稱方便區(qū)分責(zé)任
presenter通常有兩種類型的方法:
- Actions(e.g: load()):presenter的一些行為操作拣度。
- User events(e.g:queryChanged(…)):用戶觸發(fā)的操作比如在搜索框中鍵入字符或者是點(diǎn)擊列表中的某個(gè)選項(xiàng)碎绎。
你定義的action越多那么view中的邏輯也就越多螃壤。
當(dāng)用戶滾動(dòng)到列表的結(jié)尾時(shí)將調(diào)用loadMore()方法,然后presenter加載另外一頁(yè)的結(jié)果筋帖。這意味著當(dāng)用戶滾動(dòng)到結(jié)尾時(shí)奸晴,view知道必須加載新頁(yè)面。我可以命名方法onScrolledToEnd()讓具體的presenter處理具體做什么日麸。
我想說的是寄啼,在“contract設(shè)計(jì)”階段,你必須定義好每個(gè)用戶事件代箭,相應(yīng)的action是什么墩划,邏輯應(yīng)該屬于誰。
5. 不要在Presenter接口中創(chuàng)建Activity-lifecycle-style回調(diào)
我使用這個(gè)標(biāo)題的意思是presenter不應(yīng)該有像onCreate(...)嗡综,onStart()乙帮,onResume()等方法原因如下:
- 如果這么做了的話presenter將會(huì)和Activity產(chǎn)生耦合。如果我想用一個(gè)Fragment替換Activity怎么辦极景?我什么時(shí)候應(yīng)該調(diào)用presenter.onCreate(state)方法察净?在fragment的onCreate(…)、onCreateView(...)還是onViewCreated(…)中盼樟?如果我使用自定義view怎么辦氢卡?
- presenter不應(yīng)該有這么復(fù)雜的生命周期。事實(shí)上晨缴,主要的Android組件都是以這種方式設(shè)計(jì)的译秦,但并不意味著你必須也這么做。如果你有機(jī)會(huì)可以簡(jiǎn)化的話那就簡(jiǎn)化它吧击碗。
6. Presenter和view有1對(duì)1的關(guān)系
如果沒有view的話presenter就沒有意義了筑悴。presenter隨著view一起被創(chuàng)建也隨著view一起被銷毀。一個(gè)presenter管理一個(gè)view延都。
你可以通過多種方式處理presenter中view的依賴雷猪。一種方式是在presenter接口中提供像attach(View view)和detach()的方法就像之前例子中展示的那樣睛竣。不過這樣做有一個(gè)問題就是你需要注意view是否為null晰房,每次presenter用到它的時(shí)候都要檢查一下是否為null。這點(diǎn)確實(shí)有點(diǎn)煩……
我說了presenter和view是一對(duì)一的關(guān)系射沟。我們可以利用這一點(diǎn)殊者,實(shí)際上具體的presenter可以將view實(shí)例作為構(gòu)造函數(shù)的參數(shù)傳入。順便說一句验夯,你可能需要一個(gè)方法來訂閱presenter的一些事件猖吴。所以我建議定義一個(gè)方法start()(或類似的方法)來運(yùn)行Presenter中的業(yè)務(wù)。
關(guān)于detach()呢挥转?
如果你有一個(gè)叫start()的方法海蔽,那么你可能至少還需要一個(gè)來釋放依賴的方法共屈。既然我們定義訂閱presenter一些事件的方法叫start(),那么另一個(gè)方法就叫stop()吧党窜。
public interface BasePresenter<V> {
void attach(V view);
void detach();
}
public interface BasePresesnter {
void start();
void stop();
}
7. 不要在presenter中保存狀態(tài)
我的想著是要用Bundle來保存拗引。但考慮到上面的第二條準(zhǔn)則就不能這么做了。你不能將數(shù)據(jù)序列化到Bundle中幌衣,因?yàn)檫@樣的話presenter就與Android類耦合了矾削。
我說presenter應(yīng)該是無狀態(tài)的,但其實(shí)也不然豁护。在我之前描述的例子中哼凯,presenter應(yīng)該至少具有頁(yè)碼/偏移量之類的狀態(tài)。
8. 不要持久化presenter
我不喜歡這種方式主要是因?yàn)槲艺J(rèn)為presenter不是我們應(yīng)該持久化的楚里,要清楚它不是一個(gè)數(shù)據(jù)類断部。
一些建議提供了一種在配置發(fā)生改變的時(shí)候通過恢復(fù)fragments或者 Loaders的方式記住presenter的狀態(tài)。我不認(rèn)為這是最好的解決方案腻豌。通過這種方式presenter可以在方向發(fā)生變化恢復(fù)家坎,但是當(dāng)Android殺死了進(jìn)程并銷毀Activity,后者將與新的presenter一起重新創(chuàng)建吝梅。因此虱疏,該解決方案僅解決了一半的問題。
9. 為Model提供緩存以恢復(fù)視圖狀態(tài)
在我看來苏携,解決“恢復(fù)狀態(tài)”問題需要一些應(yīng)用架構(gòu)的知識(shí)做瞪。基本上右冻,作者建議使用類似Repository或任何旨在管理數(shù)據(jù)的接口來緩存網(wǎng)絡(luò)結(jié)果装蓬,范圍限定于應(yīng)用程序而不是Activity。
這個(gè)接口只是一個(gè)更聰明的Model纱扭。后者應(yīng)至少提供磁盤緩存策略和可能的內(nèi)存緩存牍帚。這樣的話,即使進(jìn)程被殺乳蛾,presenter也可以使用磁盤緩存恢復(fù)視圖狀態(tài)暗赶。
view應(yīng)該只關(guān)心必要的請(qǐng)求參數(shù)以恢復(fù)狀態(tài)。例如肃叶,在我們的示例中蹂随,我們只需要保存查詢。
現(xiàn)在因惭,你有兩個(gè)選擇:
- 你在model層中抽象這個(gè)行為岳锁,當(dāng)presenter調(diào)用repository.get(params)時(shí),如果頁(yè)面已經(jīng)在緩存中蹦魔,數(shù)據(jù)源只返回它激率,否則再調(diào)用API咳燕。
- 在contract中的presenter添加一個(gè)方法來恢復(fù)視圖狀態(tài)。restore(params)乒躺,loadFromCache(params)或reload(params)這些是描述相同動(dòng)作的不同名稱你可以隨便選一個(gè)迟郎。
結(jié)論
以上是我對(duì)應(yīng)用于Android的Model-View-Presenter架構(gòu)的看法,希望通過不斷的嘗試可以找到最佳實(shí)踐聪蘸。