Model-View-Presenter:Android指南

原文地址: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è)模式:

1
1
  • 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í)踐聪蘸。


全棧增長(zhǎng)工程師宪肖,歡迎關(guān)注
全棧增長(zhǎng)工程師,歡迎關(guān)注
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末健爬,一起剝皮案震驚了整個(gè)濱河市控乾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌娜遵,老刑警劉巖蜕衡,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異设拟,居然都是意外死亡慨仿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門纳胧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來镰吆,“玉大人,你說我怎么就攤上這事跑慕⊥蛎螅” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵核行,是天一觀的道長(zhǎng)牢硅。 經(jīng)常有香客問我,道長(zhǎng)芝雪,這世上最難降的妖魔是什么减余? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮惩系,結(jié)果婚禮上位岔,老公的妹妹穿的比我還像新娘。我一直安慰自己蛆挫,他們只是感情好赃承,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布妙黍。 她就那樣靜靜地躺著悴侵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拭嫁。 梳的紋絲不亂的頭發(fā)上可免,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天抓于,我揣著相機(jī)與錄音,去河邊找鬼浇借。 笑死捉撮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的妇垢。 我是一名探鬼主播巾遭,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼闯估!你這毒婦竟也來了灼舍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤涨薪,失蹤者是張志新(化名)和其女友劉穎骑素,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刚夺,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡献丑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了侠姑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片创橄。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖莽红,靈堂內(nèi)的尸體忽然破棺而出筐摘,到底是詐尸還是另有隱情,我是刑警寧澤船老,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布咖熟,位于F島的核電站,受9級(jí)特大地震影響柳畔,放射性物質(zhì)發(fā)生泄漏馍管。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一薪韩、第九天 我趴在偏房一處隱蔽的房頂上張望确沸。 院中可真熱鬧,春花似錦俘陷、人聲如沸罗捎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桨菜。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倒得,已是汗流浹背泻红。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留霞掺,地道東北人谊路。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像菩彬,于是被迫代替她去往敵國(guó)和親缠劝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容