如何設計MVP中的Presentation層


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

原文鏈接:http://blog.chengdazhi.com/index.php/115

我發現有很多項目設計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,那麼流程如下:

navigation command sequence

代碼長這樣:

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();
    }
}

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();
    }
}

這樣我們就可以將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;
    }
}

在這裏初始化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界面爲例分析。

spotify_1

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

spotify_2

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

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

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

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

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

spotify_3

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

MVP架構

總結一下前面的內容:

  • 一個View使用一個Presenter

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

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

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

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

Presenter生命週期

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

Citymapper_1

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

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

那麼Presenter的生命週期如何呢?這取決於與Presenter對應的組件。

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

Selltag_1

Selltag_2

Selltag_3

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

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

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

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

Presenter狀態

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

fdroid

這是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層因爲要負責各種同步之類的事情導致代碼變多。

diagram_1

改進之後的流程圖:

diagram_2

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

結論

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章