從 MVP學習代碼封裝 (4) - 給 MVP 添加 stateView狀態頁功能

本系列文章集合:從 MVP學習代碼封裝 (1) - 綜述

大家在做項目的時候絕對要面對一個需求,stateView - 缺省頁,也有叫狀態頁,我比較喜歡叫狀態頁,這里我們今天要把 stateView 這個功能集成到我們的 MVP 基礎框架中。為啥,你傻啊,這 stateView 可是基礎功能,明顯要整合到基礎業務框架中的呀,難道你要傻傻的和筆者一樣,要每個頁面都寫一遍嗎!!!呵呵,筆者在工作的第一年還真就干了這 SB 事,現在回頭想想也是感謝過去的自己冒傻氣。當時產品每天對這個 statview 改來改去的,真是要死的心都有啊,那滿滿的怨念現在都還記憶猶新啊。也是有了澤洋深刻的經歷,我對代碼封裝這塊才開始上心,真是不經歷不知道啊,這也才有了我的 MVP 這個系列的文章。

我的 stateView

stateView 的實現方案很多,和 MVP 一樣,一個公司有自己的一套,開源的庫也是有不少的,貌似這個 stateView 還真是個經久不衰的話題,不時的總是可以在各種分享場合看到關于 stateView 的文章和庫,畢竟太典型了,而且實現上還真是很有味道啊,先寫好不容易啊。

先來看看我寫的例子,我寫的效果很簡單,主要目的還是向大家表述 我寫 stateView 過程,而不是絢麗的效果:


ezgif.com-video-to-gif.gif

我寫的例子在顯示上還是很靈活的:

  • 在基類中設定 stateview 每個頁面的 layoutID
  • 然后你可以在具體的某個 activity/fragment/viewGroup 上設定某個類型狀態頁對應的 layoutID
  • 默認是在 activity/fragment/viewGroup 的根布局上顯示 stateview ,也可以設定 stateview 顯示在哪個布局上。

上面說了下我簡單封裝的這個功能,目前我看到的做 stateview 這個功能的實現思路有3種:

  • 最簡單的, 在每個 activity/fragment/viewGroup 中的 xml 布局中 include 相關的狀態頁的 xml 進來,這種方法最簡單,減少了我們很多 xml 布局中的代碼,但是 java 類中的代碼是沒有經過封裝的,還是有大量重復代碼
  • 在 activity/fragment 所屬的 window 上 addView ,這樣的思路就是開始從根本上封裝代碼了,這也應該是我們在寫代碼時首先應該想到怎么去封裝這個功能。這個在 window 上 addView 的方案,還是很有一些開源庫方案在用的,但是我覺得很實際的, 就是這個思路不甚靈活,因為 stateview 的頁面只能顯示在 整個屏幕上,那么自定義的 viewGroup 怎么辦,我們要是想 stateview 顯示在屏幕中的某一部分怎么辦呢。我覺得靈活性和可維護性,簡單易用性是代碼封裝的第一要務。
  • 承接上個思路,還是 addView ,這次不是寫死在 window 上了, 而是由用戶決定,我們在 base 基類中可以默認給一種方案,我想這也是大部分的需求了,然后在具體的 activity/fragment/viewGroup中,可以根據具體的業務需求指定不同的布局以容納 stateview

當然上面我都簡單介紹過我的實現了,會跟明顯的我就是使用的第三種。這種方式我是沒看有第三方庫用,可以全當是我自己想出來的,這里說下,筆者也是見識有限,要是各位有不同意見請見諒,畢竟在沒見過相同實現的情況下,我自己寫出了就可以當做我自己的實現了不是嘛。


實現思路

記住核心的是:我們是在現有 MVP 框架上添加一個新功能 stateview

那么根據上一節我們抽象封裝聲明周期的做法,使用組合的思路,給外部提供一個 stateview 的控制器,而不是具體的邏輯代碼。那么首先我要思考下,MVP 中哪個角色應該持有這個 stateview 控制器,顯然應該是 V 層,而不是 P 層,V 層關心的是具體顯示,那么 stateview 作為顯示的一部分,有 V 層來持有控制是合理的。

想明白這個功能加在誰的身上后,我們來思考下,這個功能應該如何合理的對外表達,顯然 stateview 是要給 view 添加各種意外狀態的頁面,那么對于具體的 view 使用者來說,這些添加各種意外狀態的頁面的功能是有 view 提供的,而不關心 view 是如何實現的。好了,說到這里我們應該清楚了,這是一種代理模式的思路,我們抽象出 stateview 的相關功能接口,然后 view 去實現接口,由 view 內部持有的具體的 stateview 實現類來實現相關方法。特點是 view 會實現 stateview 的功能接口,而不是拋出一個 stateview 對象,這是對外隱藏邏輯實現的一種做法。我們當然可以對外提供 這個stateview 對象的方法,但是這只是為了照顧一些低度需求,而主要的對外使用上我們要按照代理模式來。

上面啰嗦了幾句,是因為我們在一開始在這點上猶豫了下, 思考這個 stateview 功能應該以何種形式對外服務,是按照代理模式的思路來,還是對外直接把這個 stateview 對象拋出來。想了想,結合 MVP 的封裝思路,還是代理模式的思路更合適。

總體結構:

Snip20171220_7.png

一共涉及到這幾個類:

  • IStateView:對外功能接口
  • DefaultStateView:外層控制器,對外提供給功能
  • IStateViewProvide:內部提供 各種view 資源的接口,視為一個內部子系統
  • BaseStateViewProvide:這個子系統的 abs 基類,這里考慮要擴展,未來號維護抽象出一個功能基類出來
  • DefaultStateViewProvide:具體的子系統實現類,這個是默認實現,有其他實現可以繼承 DefaultStateViewProvide ,或是 BaseStateViewProvide
  • StateCodes:各種 view 對應的識別碼

看到這里,整個結構應該很清晰了,畢竟是個小系統,沒多少類,但是很適合練手啊,想寫好這個小功能,各方面考慮到位也是很困難的。這幾個類分以下幾個角色:

  • 外層功能接口:IStateView
  • 頂層主控制器:DefaultStateView
  • 子系統:IStateViewProvide / BaseStateViewProvide / DefaultStateViewProvide
  • 公共資源:StateCodes

不管多大型,多小型的框架,功能系統,都是由上面這幾個角色組合而成的:

  • 外層功能接口封裝了我們對外提供哪些方法功能
  • 頂層主控制器是實現了這個外層功能接口,是提供給調用者使用的,要求使用簡單,擴展方便,內部包含 N 多子系統提供具體功能,頂層主控制器值關心核心主邏輯實現。
  • 子系統是從主邏輯中抽象分離出的功能,封裝好了提供給頂層主控制器使用,這里我只是抽象出了 提供各種 view 對象的子系統出來。
  • 公共資源,這個就不用說了。

寫這個 stateView 小功能,處處體現出了組合的思路啊,不同的子系統組合在一起就是一個大系統。大系統臃腫就可以拆分成不同的小系統。

下面我們開始具體代碼部分:

這個 stateview 也算是一個小小的框架了,作為練手非常適合。從頭開始編寫一個框架,都是先開始寫主干結構,然后再去填充一個個具體功能實現。主干寫清楚了,首先我們可以清楚無誤的審視我們的結構是否合理,需要不需要再大的調整,要是都寫完了再去打動,那就是大給自己找事了。另外的好處就是我們不會混亂了,結構都有了,剩下的照著寫就好了,會杜絕我們寫著寫著突然發現某個地方寫錯了,發生結構錯誤,說實話可以大大節省我們的重復工作。

  1. 我們先來抽象 stateview 功能接口
    stateview 是 view 對外展示功能的核心,view 實現哪個接口,調用者才能通過 view 去使用哪個接口的方法,這里我們對外提供:顯示loading,網絡錯誤,數據異常,沒有數據,顯示內容幾個功能
public interface IStateView {

    void showContent();

    void showDataError();

    void showDataEmpty();

    void shownNetError();

    void showLoading();
}
  1. 把 stateview 功能提供給 V 層對象,當然是在 MVP 的框架下。

首先 V 層的跟接口 IBaseView 繼承 IStateView 接口,這樣 MVP 框架下的多有 view 對象就都有 stateview 的功能了

public interface IBaseView extends IStateView {

    DefaultStateView getStateView();

}

然后我們在 V 層的 abs 抽象基類中實現相關的 IStateView 接口的方法,這里涉及到:V 層的 abs 抽象基類如何持有,初始化 IStateView 接口具體實現類,如何添加顯示 stateview 功能的默認公共代碼。代碼只放了有關的代碼,其余的取掉了,DefaultStateView 是 IStateView 接口的具體實現類

public abstract class BaseActivity implements IBaseView {

    // 持有 IStateView 對象引用
    protected DefaultStateView mStateView;

    // 返回IStateView 對象引用
    @Override
    public DefaultStateView getStateView() {
        if (mStateView == null) {
            initStateView();
        }
        return mStateView;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 初始化時間節點
        init();
    }

    // 初始化方法
    public void init() {
        initStateView();
    }

     // 初始化方法
    public void initStateView() {
        mStateView = createStateView();
        mStateView.setRootView((ViewGroup) getWindow().getDecorView());
    }

    public DefaultStateView createStateView() {
        return new DefaultStateView(this);
    }

  // 代理 stateview 功能實現
 @Override
    public void showContent() {
        getStateView().showContent();
    }

    @Override
    public void showDataError() {
        getStateView().showDataError();
    }

    @Override
    public void showDataEmpty() {
        getStateView().showDataEmpty();
    }

    @Override
    public void shownNetError() {
        getStateView().shownNetError();
    }

    @Override
    public void showLoading() {
        getStateView().showLoading();
    }

經過上面2部的編寫,我們就完成了 stateview 功能 在MVP 框架中的部分,下面就是 stateview 自身的實現了。

  1. 實現 stateview 控制類

這個 stateview 控制類用于對外提供相關功能調用,多說點這個也可以叫門板類,是外觀設計模式的概念,外觀設計模式把一個復雜的框架或是功能稱為:系統。這里我們的 stateview 功能也是可以稱之為一個系統,不管這個系統內部實現有多么的復雜,對調用者來說我只要能簡單的調用就好了,才不關心你怎么實現的,那么這個門板類就是給調用者使用的,簡要求簡單易用,不要拖泥帶水。

DefaultStateView 這個是我們的控制類,實現 IStateView 接口

public class DefaultStateView implements IStateView {

    public Context context;

    private ViewGroup rootView;
    private IStateViewProvide mStateViewProvide;

    public DefaultStateView(Context context) {
        this.context = context;
        init();
    }

    private void init() {
        initStateViewProvide(context);
    }

    private void initStateViewProvide(Context context) {
        mStateViewProvide = new DefaultStateViewProvide(context);
    }

    public IStateViewProvide getStateViewProvide() {
        return mStateViewProvide;
    }

    public void setContext(Context context) {
        this.context = context;
        invalidata(context);
    }

    @Override
    public void showContent() {
        if (rootView == null) {
            return;
        }
        if (StateCodes.CONTENT.code != mStateViewProvide.getCurrentStateViewCode()) {
            cleanCurrentStateView();
        }
    }

    @Override
    public void showDataError() {
        if (rootView == null) {
            return;
        }
        if (StateCodes.DATA_ERROR.code != mStateViewProvide.getCurrentStateViewCode()) {
            addDataErrorStateView();
        }
    }

    @Override
    public void showDataEmpty() {
        if (rootView == null) {
            return;
        }
        if (StateCodes.DATA_EMPTY.code != mStateViewProvide.getCurrentStateViewCode()) {
            addDataEmptyStateView();
        }
    }

    @Override
    public void shownNetError() {
        if (rootView == null) {
            return;
        }
        if (StateCodes.NET_ERROR.code != mStateViewProvide.getCurrentStateViewCode()) {
            addNetErrorStateView();
        }
    }

    @Override
    public void showLoading() {
        if (rootView == null) {
            return;
        }
        if (StateCodes.LOADING.code != mStateViewProvide.getCurrentStateViewCode()) {
            addloaingStateView();
        }
    }

    public void cleanCurrentStateView() {

        View currentStateView = mStateViewProvide.getCurrentStateView();
        if (currentStateView == null || rootView == null) {
            return;
        }
        rootView.removeView(currentStateView);
        mStateViewProvide.setCurrentStateViewCode(StateCodes.CONTENT.code);
    }

    public void setRootView(ViewGroup rootView) {
        this.rootView = rootView;
    }

    private void addDataErrorStateView() {
        cleanCurrentStateView();
        addStateViewByStateViewCode(StateCodes.DATA_ERROR.code);
    }

    private void addDataEmptyStateView() {
        cleanCurrentStateView();
        addStateViewByStateViewCode(StateCodes.DATA_EMPTY.code);
    }

    private void addNetErrorStateView() {
        cleanCurrentStateView();
        addStateViewByStateViewCode(StateCodes.NET_ERROR.code);
    }

    private void addloaingStateView() {
        cleanCurrentStateView();
        addStateViewByStateViewCode(StateCodes.LOADING.code);
    }

    private void addStateViewByStateViewCode(int stateViewCode) {
        View view = mStateViewProvide.getStateViewByCode(stateViewCode);
        if (view == null || rootView == null) {
            return;
        }
        rootView.addView(view);
        mStateViewProvide.setCurrentStateViewCode(stateViewCode);
    }

    private void invalidata(Context context) {
        if (context == null) {
            return;
        }
        mStateViewProvide = new DefaultStateViewProvide(context);
    }

}
  1. 實現 stateview 相關頁面提供器

看過第三部的,可以看到有一個關鍵功能類: mStateViewProvide,這是提供相關 view 的功能類,這里覺得應該把 view 提供的功能從控制器類分離出來,再抽象出一個功能類來,因為外層控制器封裝的是整個系統的邏輯,而期中較為復雜的代碼片段可以再次封裝成為一個這個系統中一個子系統,這樣代碼結構可以更清晰,編寫頁簡單容易一些,不容易亂。這里 mStateViewProvide 這個對象就是對外提供 各種狀態 view 的子系統對象。

考慮到這里以后會有擴展,所以靈活一些,封裝出一個 abs 基類,放一些基礎功能代碼

public abstract class BaseStateViewProvide implements IStateViewProvide {

    protected Context context;

    protected int curState = StateCodes.CONTENT.code;
    protected Map<Integer, Integer> stateIDs;
    protected Map<Integer, View> stateViews;

    public BaseStateViewProvide(Context context) {
        this.context = context;
        init();
    }

    @Override
    public void setContext(Context context) {
        this.context = context;
    }

    public int getCurState() {
        return curState;
    }

    public Map<Integer, Integer> getStateIDs() {
        if (stateIDs == null) {
            initMap();
        }
        return stateIDs;
    }

    public Map<Integer, View> getStateViews() {
        if (stateViews == null) {
            initMap();
        }
        return stateViews;
    }

    public View getStateView(int stateViewCode) {

        if (stateViews == null) {
            init();
        }
        if (stateViews == null || stateViews.size() == 0) {
            return null;
        }
        return stateViews.get(stateViewCode);
    }

    public int getStateViewID(int stateViewCode) {

        if (stateIDs == null) {
            init();
        }
        if (stateIDs == null || stateIDs.size() == 0) {
            return 0;
        }
        return stateIDs.get(stateViewCode);
    }

    public View inflateView(int stateViewCode) {

        if (context == null) {
            return null;
        }
        int stateViewCode2 = stateViewCode;
        Integer integer = getStateIDs().get(stateViewCode);
        View view = LayoutInflater.from(context).inflate(integer, null, false);

        if (view == null) {
            return null;
        }

        getStateViews().put(stateViewCode, view);
        return view;
    }

    private void init() {
        initMap();
        initData();
    }

    private void initMap() {
        if (stateIDs == null) {
            stateIDs = new HashMap<>();
        }
        if (stateViews == null) {
            stateViews = new HashMap<>();
        }
    }

    protected abstract void initData();

    protected void invalidateData() {
        if (stateViews == null || stateIDs == null) {
            initMap();
        }
        stateViews.clear();
        stateIDs.clear();
        initData();
    }

    protected void setStateVIewIDAndCleanStateView(int stateVIewCode,int stateVIewID) {
        getStateIDs().remove(stateVIewCode);
        getStateIDs().put(stateVIewCode, stateVIewID);
        getStateViews().remove(stateVIewCode);
    }

然后具體實現這個功能 DefaultStateViewProvide ,核心就在于 重寫 initData() 這個方法,在這個方法里綁定各種 狀態 view 的 layoutID,未來擴展時可以考慮繼承這個 DefaultStateViewProvide 類,重寫 initData() ,也可以繼承 BaseStateViewProvide 這個 abs 基類提供更多的功能,這里涉及到重寫 外層控制器了,要不也不會設計到寫個新的功能類了

public class DefaultStateViewProvide extends BaseStateViewProvide {

    public DefaultStateViewProvide(Context context) {
        super(context);
    }

    @Override
    protected void initData() {
        getStateIDs().put(StateCodes.LOADING.code, R.layout.layout_stateview_loading);
        getStateIDs().put(StateCodes.DATA_ERROR.code, R.layout.layout_stateview_dataerror);
        getStateIDs().put(StateCodes.DATA_EMPTY.code, R.layout.layout_stateview_dataempty);
        getStateIDs().put(StateCodes.NET_ERROR.code, R.layout.layout_stateview_neterror);
    }

    public void setmDataErrorViewID(int mDataErrorViewID) {
        setStateVIewIDAndCleanStateView(StateCodes.DATA_ERROR.code, mDataErrorViewID);
    }

    public void setmDataEmptyViewID(int mDataEmptyViewID) {
        setStateVIewIDAndCleanStateView(StateCodes.DATA_EMPTY.code, mDataEmptyViewID);
    }

    public void setmNetErrorViewID(int mNetErrorViewID) {
        setStateVIewIDAndCleanStateView(StateCodes.NET_ERROR.code, mNetErrorViewID);
    }

    public void setmloadingViewID(int mLoadingViewID) {
        setStateVIewIDAndCleanStateView(StateCodes.LOADING.code, mLoadingViewID);
    }

    @Override
    public View getCurrentStateView() {
        return getStateViews().get(curState);
    }

    @Override
    public int getCurrentStateViewCode() {
        return curState;
    }

    @Override
    public void setCurrentStateViewCode(int stateViewCode) {
        curState = stateViewCode;
    }

    @Override
    public View getStateViewByCode(int stateViewCode) {

        View view = getStateViews().get(stateViewCode);
        if (view == null) {
            view = inflateView(stateViewCode);
        }

        return view;
    }

}

總結

最后了,也沒啥說的,大伙沒事多看看設計模式,設計模式要靈活應用,不必分得按照設計模式的例子來,只要設計模式的恩那個給我們提供思路就行。

項目地址: BW-MVPDemo / step2_2 這部分。


其他資料

這個單元寫完有些時間了,又看到一些新的文章,這里整理一下

  1. 有個朋友在總結自己的 stateview 時有一些感想很贊
  1. 找到一個開源項目和我的思路基本吻合,卻別在于管理 stateview 地方不太一樣,我的是在頁面 base 層提供管理的,這位兄弟的是在頁面層里,想了想,各有利弊,應該在頁面層之外也能提供靈活優雅的使用,這點之后會考慮重構一下

參考項目:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容