如何設計MVP中的Presentation層

renrukun07 8年前發布 | 16K 次閱讀 安卓開發 Android開發 移動開發

來自: http://blog.chengdazhi.com/index.php/115

原文鏈接:http://panavtec.me/modeling-presentation-layer/

我發現有很多項目設計MVP架構時,分不清哪些代碼屬于Presenter而哪些代碼屬于View(UI),這就是我寫這篇文章的目的。

Android view vs View vs 界面

先區分一下Android View、View、界面的區別

  • Android View: 只是繼承android.view.View的Android組件。

  • View:接口,用于由presenter向View實現類通信,你可以在Android組件中實現它。有時最好直接使用Activity,Fragment或自定義View。

  • 界面:界面是面向用戶的概念。比如要在手機上進行界面間切換時,我們在代碼中可以通過多種方式實現,如Activity到Activity或一個Activity內部的Fragment/View進行切換。所以這個概念基于用戶的視覺,包括了所有View中能看到的東西。

切換界面

界面間的切換可以是兩個Fragment、兩個Activity、打開對話框、啟動新Activity等等。當然切換的具體實現原理不屬于這篇文章的內容,而進行切換操作則是Presentation層的職責。Presenter應該知道要做什么,而它的實現類要知道怎么完成。在這個例子中,要做的就是切換界面,完成方式就是啟動新的Activity。

但這樣會有一個問題。Presentation層是純java代碼,所以Presenter中不應該有任何與安卓相關的代碼。那怎么完成界面的切換呢?通過抽象。這里可以寫一個只有一個navigate()方法的接口NavigationCommand。在需要時我們在Presenter中調用這個接口的navigate()方法,然后在Activity中實現這個接口。假設要從Activity A切到Activity B,那么流程如下:

代碼長這樣:

View層

ActivityA.java

public class ActivityA extends Activity {
    @OnClick(R.id.someButton)
    public void onSomeButtonClicked() {
    presenter.onSomeButtonClicked();
}

ToActivityB.java

public class ToActivityB implements NavigationCommand {
    private final Activity currentActivity;

public ToActivityB(Activity activity) {
    currentActivity = activity;
}

@Override
public void navigate() {
    currentActivity.startActivity();
}

}</pre>

Presentation層

NavigationCommand.interface

public interface NavigationCommand {
    public void navigate();
}

PresenterA.java

public class PresenterA {
    private final NavigationCommand toBNavigation;

public PresenterA(NavigationCommand toBNavigation) {
    this.toBNavigation = toBNavigation;
}

public void onSomeButtonClicked() {
    toBNavigation.navigate();
}

}</pre>

這樣我們就可以將VP兩層解耦。這里將切換到一個Activity的代碼提取出來,可以復用,我們可以通過注入NavigationCommand方法來測試Presenter,而且就算要跳轉的頁面變了,Presenter的代碼也不變。這也符合Open Close原則。

另一個問題就是當一個Presenter中出現多個NavigationCommand時,構造方法就開始變得詭異了。

public class PresenterA {
    private final NavigationCommand toBNavigation;
    private final NavigationCommand toCNavigation;

public PresenterA(NavigationCommand toBNavigation, NavigationCommand toCNavigation) {
    this.toBNavigation = toBNavigation;
    this.toCNavigation = toCNavigation;
}

}</pre>

在這里初始化Presenter的類很難搞清楚兩個NavigationCommand之間的順序,似乎只能通過名字來辨識,這里其實可以再寫一個接口繼承NavigationCommand來專門管理一類特定的切換,或者如果你使用依賴注入框架的話也可以指定參數的類型。

有時需要在切換界面時傳遞一些參數,這時就要改動一下NavigationCommand的代碼:

public interface ToScreenBNavigationCommand extends NavigationCommand {
    void setMyParameterToNavigate(String parameter);
}

這樣只需要在Presenter中在調用navigate()方法之前調用設置參數的方法就行了。

這個idea歸功于 Pedro 的項目 EffectiveAndroidUI

一個界面中有多個View

Android中一個View可能由不同的組件實現,但這不影響Presenter。那一個界面中可以有多個View嗎?當然可以!那如何在一個Activity中寫多個View/Presenter呢?下面以Browse Spotify界面為例分析。

這個界面里有一個橫向的滾動條顯示不同的歌單,然后是一個有多重選項的菜單,在底部有一個正在播放的歌曲。當然每個人看一個界面會有不同的理解,但這不是關鍵,所以我們來考慮如何按上述三個組件分開View和Presenter。

黃色是歌單,紅色是菜單,藍色是正在播放。

不過為什么要分開呢?分開寫View/Presenter與合在一起寫一個有所有操作的Presenter有多大區別呢?這時要考慮到誰負責填充這些View,以及如何復用組件。這三個組件是完全不同的組件,有不同的功能、操作與邏輯代碼,它們都會在其他界面被用到。

所以一個界面可以有多個View/Presenter,因為一個界面可能包括了許多組件而且可能負責許多操作,這個是設計師的事。要記住每一項責任就是一個潛在的發生改變的原因,而上述這三個View都很可能發生改變。

一個View可以有2個實現類嗎?

當然!對同一個Presenter的View可以有多個實現類。再以Spotify舉例,剛才的界面的下方有一個正在播放的欄,當你點擊時出現下面這個界面:

是不是只是換了一種展示的方式呢?所以或許我們可以繼續使用同樣的Presenter并在另一個Android組件中實現View接口。不過這個界面似乎有更多的功能,那要不要把這些新功能加進這個Presenter呢?這個視情況而定,有多種方案:一是將Presenter整合負責不同操作,二是寫兩個Presenter分別負責操作和展示,三是寫一個Presenter包含所有操作(在兩個View相似時)。記住沒有完美的解決方案,編程的過程就是讓步的過程。

MVP架構

總結一下前面的內容:

  • 一個View使用一個Presenter

  • 一個界面可以有多個View/Presenter

  • 一個View可以被多次實現以使用同一個Presenter

  • 一個Android組件可以實現一個View。如果要同時實現兩個View接口,或許這兩個View最好一起來展示一個組件,或是你應該將View的實現分割,分別對應兩個View接口。

下面來看一下其他的概念。

Presenter生命周期

下面這張截圖來自Citymapper,當你點擊“帶我去那”按鈕的時候就會打開一個讓你選擇開始結束位置的界面。

如何分解這個界面呢?我首先想到的事情就是:如果沒有結束位置,那起始位置還有意義嗎?應該沒有。所以我可以寫一個Presenter “PickLocation”來監聽開始和結束位置是否填寫。而后寫一個Activity包含兩個Fragment能在ViewPager中切換,這就組成了View層。兩個Fragment都可以調用同一個Presenter的startLocationChanged()和endLocationChanged()方法。

如果此時設計改了,不再是兩個tab了,而是一個分為兩步的表單。這是需要將選擇開始位置的Fragment替換為選擇結束位置的Fragment。View層的代碼改變了,但Presenter不變。設計可以千變萬化,再比如分屏顯示兩個地圖,但都不會改變Presenter的代碼。

那么Presenter的生命周期如何呢?這取決于與Presenter對應的組件。

我們先看一下Selltag應用,這是一個二手交易應用。下面是舊版應用創建商品的截圖:

這就是一個三步表單。除了西班牙詞匯外還都挺清晰地。”Siguiente”是”下一步”,”Publicar”是“發布”。

在第一步中,先給商品添加一些圖片。第二步中要填寫標題、描述和價格。最后點擊發布按鈕,商品就進入交易市場了。

在我給這個表單建立的模型中只有一個Presenter:“PublishProductPresenter”。這個Presenter代表了整個“發布商品”的概念。而這個表單在平板上該如何顯示呢?或許這三步可以整合在一個界面中,畢竟屏幕變大了。但不要看到界面變了就改架構,這里只是View層變了,因為Presentation層只需要處理用戶事件,代碼不變。

這里有一個問題是如果只用一個Presenter的話,在不同的界面間如何傳遞數據呢?是在后面界面的Presenter保留前面界面Presenter的引用嗎?還是創建一個Presenter讓三個Activity共享?這樣出了bug很難調試啊。這里也可以將這三個界面寫成Fragment放在一個Activity里。

Presenter狀態

當屏幕方向改變的時候,Activity和Presenter都會被銷毀,所以要不要給Presenter設置狀態呢?其實添加Presenter狀態還不如修改一下Model層的代碼。為什么這么講,請看這個例子:

這是F-Droid的Android版,一個只包含開源項目的開源市場。當Available欄目刷新時會發起一次網絡請求。假如這時屏幕旋轉,并且Presenter是沒有狀態的話,List就會被重新加載。如何解決這個問題呢?其實不難,可以把上次的response緩存進內存或disk中,并指定一個ttl(time to live有效期)。但不要將從網絡獲取的內容存進Presenter里,因為如果要重建Presenter的話,就需要重新發送一遍請求。綜上,我不喜歡給我的Presenter添加狀態。

回調地獄Callback Hell

Callback Hell是人們談論Presentation層時經常討論的問題。許多有關回調地獄的問題都是因為Presenter的任務太重。不要把Model層的任務放在Presenter里,Presentation層只應該調用Model層的方法,由Model層完成諸如同步等操作。在使用RxJava或Jdeferred等第三方庫之前請思考是真的需要還是只是必須通過這些庫把整個系統粘在一起。

為了描述上述問題我造了一個例子:想象一個系統,只在服務端返回true時加載并展示一個產品組成的列表。下面第一個圖所展示的流程主要在錯在兩個地方:第一是Presenter不應該知道服務端返回的flag,這個是Model層的事。第二是presentation層因為要負責各種同步之類的事情導致代碼變多。

改進之后的流程圖:

這樣一來兩個問題就解決了,而且如果未來不再需要flag了就只需要修改action就行了。

結論

設計Presentation層的架構很簡單,但你需要知道什么代碼歸Presenter什么歸Model。當你有一個巨型Presenter時,想想真的是界面需要響應的事件太多還是你的Presenter干了Model的事。

</div>

 本文由用戶 renrukun07 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!