Android狀態管理優化

目錄介紹

  • 01.界面狀態有哪些
  • 02.採用include方式管理
  • 03.在Base類中處理邏輯
  • 04.如何降低偶性和入侵性
  • 05.封裝低入侵性狀態庫

    • 5.1 自定義幀佈局
    • 5.2 自定義狀態管理器
    • 5.3 如何管理多種狀態
  • 06.封裝庫極致優化點說明

    • 6.1 用ViewStub顯示佈局
    • 6.2 處理重新加載邏輯
  • 07.如何使用該封裝庫

好消息

  • 博客筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 鏈接地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

01.界面狀態有哪些

  • 在Android中,不管是activity或者fragment,在加載視圖的時候都有可能會出現多種不同的狀態頁面View。比如常見的就有這些:

    • 內容界面,也就是正常有數據頁面
    • 加載數據中,加載loading
    • 加載數據錯誤,請求數據異常
    • 加載後沒有數據,請求數據爲空
    • 沒有網絡,網絡異常
  • 同時,思考一下幾個問題。

    • 怎樣切換界面狀態?有些界面想定製自定義狀態?狀態如何添加點擊事件?下面就爲解決這些問題!
  • 爲何要這樣?

    • 一般在加載網絡數據時,需要用戶等待的場景,顯示一個加載的Loading動畫可以讓用戶知道App正在加載數據,而不是程序卡死,從而給用戶較好的使用體驗。
    • 當加載的數據爲空時顯示一個數據爲空的視圖、在數據加載失敗時顯示加載失敗對應的UI並支持點擊重試會比白屏的用戶體驗更好一些。
    • 加載中、加載失敗、空數據等不同狀態頁面風格,一般來說在App內的所有頁面中需要保持一致,也就是需要做到全局統一。

02.採用include方式管理

  • 直接把這些界面include到main界面中,然後動態去切換界面,具體一點的做法如下所示。

    • 在佈局中,會存放多個狀態的佈局。然後在頁面中根據邏輯將對應的佈局給顯示或者隱藏,但存在諸多問題。
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <!--正常時佈局-->
        <include layout="@layout/activity_content"/>
        <!--加載loading佈局-->
        <include layout="@layout/activity_loading"/>
        <!--異常時佈局-->
        <include layout="@layout/activity_error"/>
        <!--空數據時佈局-->
        <include layout="@layout/activity_emptydata"/>
    
    </LinearLayout>
  • 存在的問題分析

    • 後來發現這樣處理不容易複用到其他項目中,代碼複用性很低
    • 在activity中處理這些狀態的顯示和隱藏比較亂
    • 調用setContentView方法時,是將所有的佈局給加載繪製出來。其實沒有必要
    • 如果將邏輯寫在BaseActivity中,利用子類繼承父類特性,在父類中寫切換狀態,但有些界面如果沒有繼承父類,又該如何處理

03.在Base類中處理邏輯

  • 首先是定義一個自定義的控件,比如把它命名成LoadingView,然後在這個裏面include一個佈局,該佈局包含一些不同狀態的視圖。代碼思路如下所示:

    public class LoadingView extends LinearLayout implements View.OnClickListener {
    
        public static final int LOADING = 0;
        public static final int STOP_LOADING = 1;
        public static final int NO_DATA = 2;
        public static final int NO_NETWORK = 3;
        public static final int GONE = 4;
        public static final int LOADING_DIALOG = 5;
    
        private TextView mNoDataTextView;
        private ProgressBar mLoadingProgressBar;
        private RelativeLayout mRlError;
        private LinearLayout mLlLoading;
        private View mView;
    
        private OnRefreshListener mListener;
    
        public void setRefrechListener(OnRefreshListener mListener) {
            this.mListener = mListener;
        }
    
        public interface OnRefreshListener {
            void refresh();
        }
    
        public LoadingView(Context context) {
            super(context);
            init(context);
        }
    
        public LoadingView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context);
        }
    
        private void init(Context context) {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            mView = inflater.inflate(R.layout.common_loading_get, this);
            mLoadingProgressBar = (ProgressBar) mView.findViewById(R.id.mLoadingProgressBar);
            mNoDataTextView  = (TextView) mView.findViewById(R.id.mNoDataTextView);
            mLlLoading = (LinearLayout) mView.findViewById(R.id.ll_loading);
            mRlError = (RelativeLayout) mView.findViewById(R.id.rl_error);
            mRlError.setOnClickListener(this);
            setStatue(GONE);
        }
    
        public void setStatue(int status) {
            setVisibility(View.VISIBLE);
            try {
                if (status == LOADING) {//更新
                    mRlError.setVisibility(View.GONE);
                    mLlLoading.setVisibility(View.VISIBLE);
                } else if (status == STOP_LOADING) {
                    setVisibility(View.GONE);
                } else if (status == NO_DATA) {//無數據情況
                    mRlError.setVisibility(View.VISIBLE);
                    mLlLoading.setVisibility(View.GONE);
                    mNoDataTextView.setText("暫無數據");
                } else if (status == NO_NETWORK) {//無網絡情況
                    mRlError.setVisibility(View.VISIBLE);
                    mLlLoading.setVisibility(View.GONE);
                    mNoDataTextView.setText("網絡加載失敗,點擊重新加載");
                } else {
                    setVisibility(View.GONE);
                }
            } catch (OutOfMemoryError e) {
            }
        }
    
        @Override
        public void onClick(View v) {
            mListener.refresh();
            setStatue(LOADING);
        }
    }
  • 然後在BaseActivity/BaseFragment中封裝LoadingView的初始化邏輯,並封裝加載狀態切換時的UI顯示邏輯,暴露給子類以下方法:

    void showLoading(); //調用此方法顯示加載中的動畫
    void showLoadFailed(); //調用此方法顯示加載失敗界面
    void showEmpty(); //調用此方法顯示空頁面
    void onClickRetry(); //子類中實現,點擊重試的回調方法
    • 在BaseActivity/BaseFragment的子類中可通過上一步的封裝比較方便地使用加載狀態顯示功能。這種使用方式耦合度太高,每個頁面的佈局文件中都需要添加LoadingView,使用起來不方便而且維護成本較高,比如說有時候異常狀態的佈局各個頁面不同,那麼難以自定義處理,修改起來成本較高。
    • 同時如果是要用這種狀態管理工具,則需要在需要的頁面佈局中添加該LoadingView視圖。這樣也能夠完成需求,但是感覺有點麻煩。
  • 具體如何使用它進行狀態管理呢?可以看到在對應的佈局中需要寫上LoadingView

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.cheoo.app.view.recyclerview.TypeRecyclerView
            android:id="@+id/mRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:overScrollMode="never"
            android:scrollbars="none">
    
        </com.cheoo.app.view.recyclerview.TypeRecyclerView>
    
        <com.cheoo.app.view.LoadingView
            android:id="@+id/mLoadingView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </RelativeLayout>
  • 那麼,如果某個子類不想繼承BaseActivity類,如何使用該狀態管理器呢?代碼中可以這種使用。

    mLoadingView  = (LoadingView)findViewById(R.id.mLoadingView);
    mLoadingView.setStatue(LoadingView.LOADING);
    mLoadingView.setStatue(LoadingView.STOP_LOADING);
    mLoadingView.setStatue(LoadingView.NO_NETWORK);
    mLoadingView.setStatue(LoadingView.NO_DATA);

04.如何降低偶性和入侵性

  • 讓View狀態的切換和Activity徹底分離開,必須把這些狀態View都封裝到一個管理類中,然後暴露出幾個方法來實現View之間的切換。
    在不同的項目中可以需要的View也不一樣,所以考慮把管理類設計成builder模式來自由的添加需要的狀態View。
  • 那麼如何降低耦合性,讓代碼入侵性低。方便維護和修改,且移植性強呢?大概具備這樣的條件……

    • 可以運用在activity或者fragment中
    • 不需要在佈局中添加LoadingView,而是統一管理不同狀態視圖,同時暴露對外設置自定義狀態視圖方法,方便UI特定頁面定製
    • 支持設置自定義不同狀態視圖,即使在BaseActivity統一處理狀態視圖管理,也支持單個頁面定製
    • 在加載視圖的時候像異常和空頁面能否用ViewStub代替,這樣減少繪製,只有等到出現異常和空頁面時,纔將視圖給inflate出來
    • 當頁面出現網絡異常頁面,空頁面等,頁面會有交互事件,這時候可以設置點擊設置網絡或者點擊重新加載等等

05.封裝低入侵性狀態庫

5.1 自定義幀佈局

  • 首先需要自定義一個狀態StateFrameLayout佈局,它是繼承FrameLayout。在這個類中,目前是設置五種不同狀態的視圖佈局,主要的功能操作是顯示或者隱藏佈局。爲了後期代碼維護性,根據面向對象的思想,類儘量保證單一職責,所以關於狀態切換,以及設置自定義狀態佈局,把這個功能分離處理,放到一個StateLayoutManager中處理。

    • 看代碼可知,這個類的功能非常明確,就是隱藏或者展示視圖作用。
    /**
     * <pre>
     *     @author yangchong
     *     blog  : https://github.com/yangchong211/YCStateLayout
     *     time  : 2017/7/6
     *     desc  : 自定義幀佈局
     *     revise:
     * </pre>
     */
    public class StateFrameLayout extends FrameLayout {
    
        /**
         *  loading 加載id
         */
        public static final int LAYOUT_LOADING_ID = 1;
    
        /**
         *  內容id
         */
        public static final int LAYOUT_CONTENT_ID = 2;
    
        /**
         *  異常id
         */
        public static final int LAYOUT_ERROR_ID = 3;
    
        /**
         *  網絡異常id
         */
        public static final int LAYOUT_NETWORK_ERROR_ID = 4;
    
        /**
         *  空數據id
         */
        public static final int LAYOUT_EMPTY_DATA_ID = 5;
    
        /**
         *  存放佈局集合
         */
        private SparseArray<View> layoutSparseArray = new SparseArray<>();
    
        //private HashMap<Integer,View> map = new HashMap<>();
    
        /**
         *  佈局管理器
         */
        private StateLayoutManager mStatusLayoutManager;
    
    
        public StateFrameLayout(Context context) {
            super(context);
        }
    
        public StateFrameLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public StateFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
    
        public void setStatusLayoutManager(StateLayoutManager statusLayoutManager) {
            mStatusLayoutManager = statusLayoutManager;
            //添加所有的佈局到幀佈局
            addAllLayoutToRootLayout();
        }
    
        private void addAllLayoutToRootLayout() {
            if (mStatusLayoutManager.contentLayoutResId != 0) {
                addLayoutResId(mStatusLayoutManager.contentLayoutResId, StateFrameLayout.LAYOUT_CONTENT_ID);
            }
            if (mStatusLayoutManager.loadingLayoutResId != 0) {
                addLayoutResId(mStatusLayoutManager.loadingLayoutResId, StateFrameLayout.LAYOUT_LOADING_ID);
            }
    
            if (mStatusLayoutManager.emptyDataVs != null) {
                addView(mStatusLayoutManager.emptyDataVs);
            }
            if (mStatusLayoutManager.errorVs != null) {
                addView(mStatusLayoutManager.errorVs);
            }
            if (mStatusLayoutManager.netWorkErrorVs != null) {
                addView(mStatusLayoutManager.netWorkErrorVs);
            }
        }
    
        private void addLayoutResId(@LayoutRes int layoutResId, int id) {
            View resView = LayoutInflater.from(mStatusLayoutManager.context).inflate(layoutResId, null);
            layoutSparseArray.put(id, resView);
            addView(resView);
        }
    
        /**
         *  顯示loading
         */
        public void showLoading() {
            if (layoutSparseArray.get(LAYOUT_LOADING_ID) != null) {
                showHideViewById(LAYOUT_LOADING_ID);
            }
        }
    
        /**
         *  顯示內容
         */
        public void showContent() {
            if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null) {
                showHideViewById(LAYOUT_CONTENT_ID);
            }
        }
    
        /**
         *  顯示空數據
         */
        public void showEmptyData(int iconImage, String textTip) {
            if (inflateLayout(LAYOUT_EMPTY_DATA_ID)) {
                showHideViewById(LAYOUT_EMPTY_DATA_ID);
                emptyDataViewAddData(iconImage, textTip);
            }
        }
    
    
        /**
         * 根據ID顯示隱藏佈局
         * @param id                        id值
         */
        private void showHideViewById(int id) {
            for (int i = 0; i < layoutSparseArray.size(); i++) {
                int key = layoutSparseArray.keyAt(i);
                View valueView = layoutSparseArray.valueAt(i);
                //顯示該view
                if(key == id) {
                    valueView.setVisibility(View.VISIBLE);
                    if(mStatusLayoutManager.onShowHideViewListener != null) {
                        mStatusLayoutManager.onShowHideViewListener.onShowView(valueView, key);
                    }
                } else {
                    if(valueView.getVisibility() != View.GONE) {
                        valueView.setVisibility(View.GONE);
                        if(mStatusLayoutManager.onShowHideViewListener != null) {
                            mStatusLayoutManager.onShowHideViewListener.onHideView(valueView, key);
                        }
                    }
                }
            }
        }
    
        /**
         * 這個是處理ViewStub的邏輯,主要有網絡異常佈局,加載異常佈局,空數據佈局
         * @param id                        佈局id
         * @return                          布爾值
         */
        private boolean inflateLayout(int id) {
            boolean isShow = true;
            //如果爲null,則直接返回false
            if (layoutSparseArray.get(id) == null) {
                return false;
            }
            switch (id) {
                case LAYOUT_NETWORK_ERROR_ID:
                    if (mStatusLayoutManager.netWorkErrorVs != null) {
                        View view = mStatusLayoutManager.netWorkErrorVs.inflate();
                        retryLoad(view, mStatusLayoutManager.netWorkErrorRetryViewId);
                        layoutSparseArray.put(id, view);
                        isShow = true;
                    } else {
                        isShow = false;
                    }
                    break;
                case LAYOUT_ERROR_ID:
                    if (mStatusLayoutManager.errorVs != null) {
                        View view = mStatusLayoutManager.errorVs.inflate();
                        if (mStatusLayoutManager.errorLayout != null) {
                            mStatusLayoutManager.errorLayout.setView(view);
                        }
                        retryLoad(view, mStatusLayoutManager.errorRetryViewId);
                        layoutSparseArray.put(id, view);
                        isShow = true;
                    } else {
                        isShow = false;
                    }
                    break;
                case LAYOUT_EMPTY_DATA_ID:
                    if (mStatusLayoutManager.emptyDataVs != null) {
                        View view = mStatusLayoutManager.emptyDataVs.inflate();
                        if (mStatusLayoutManager.emptyDataLayout != null) {
                            mStatusLayoutManager.emptyDataLayout.setView(view);
                        }
                        retryLoad(view, mStatusLayoutManager.emptyDataRetryViewId);
                        layoutSparseArray.put(id, view);
                        isShow = true;
                    } else {
                        isShow = false;
                    }
                    break;
                default:
                    break;
            }
            return isShow;
        }
    }

5.2 自定義狀態管理器

  • 上面狀態的自定義佈局創建出來了,而且隱藏和展示都做了。那麼如何控制設置自定義視圖佈局,還有如何控制不同佈局之間切換,那麼就需要用到這個類呢!https://github.com/yangchong211/YCStateLayout

    • loadingLayoutResId和contentLayoutResId代表等待加載和顯示內容的xml文件
    • 幾種異常狀態要用ViewStub,因爲在界面狀態切換中loading和內容View都是一直需要加載顯示的,但是其他的3個只有在沒數據或者網絡異常的情況下才會加載顯示,所以用ViewStub來加載他們可以提高性能。
    • 採用builder模式,十分簡單,代碼如下所示。創建StateFrameLayout對象,然後再設置setStatusLayoutManager,這一步操作是傳遞一個Manager對象到StateFrameLayout,建立連接。
    public final class StateLayoutManager {
    
        final Context context;
    
        final int netWorkErrorRetryViewId;
        final int emptyDataRetryViewId;
        final int errorRetryViewId;
        final int loadingLayoutResId;
        final int contentLayoutResId;
        final int retryViewId;
        final int emptyDataIconImageId;
        final int emptyDataTextTipId;
        final int errorIconImageId;
        final int errorTextTipId;
    
        final ViewStub emptyDataVs;
        final ViewStub netWorkErrorVs;
        final ViewStub errorVs;
        final AbsViewStubLayout errorLayout;
        final AbsViewStubLayout emptyDataLayout;
    
        private final StateFrameLayout rootFrameLayout;
        final OnShowHideViewListener onShowHideViewListener;
        final OnRetryListener onRetryListener;
    
        public static Builder newBuilder(Context context) {
            return new Builder(context);
        }
    
        private StateLayoutManager(Builder builder) {
            this.context = builder.context;
            this.loadingLayoutResId = builder.loadingLayoutResId;
            this.netWorkErrorVs = builder.netWorkErrorVs;
            this.netWorkErrorRetryViewId = builder.netWorkErrorRetryViewId;
            this.emptyDataVs = builder.emptyDataVs;
            this.emptyDataRetryViewId = builder.emptyDataRetryViewId;
            this.errorVs = builder.errorVs;
            this.errorRetryViewId = builder.errorRetryViewId;
            this.contentLayoutResId = builder.contentLayoutResId;
            this.onShowHideViewListener = builder.onShowHideViewListener;
            this.retryViewId = builder.retryViewId;
            this.onRetryListener = builder.onRetryListener;
            this.emptyDataIconImageId = builder.emptyDataIconImageId;
            this.emptyDataTextTipId = builder.emptyDataTextTipId;
            this.errorIconImageId = builder.errorIconImageId;
            this.errorTextTipId = builder.errorTextTipId;
            this.errorLayout = builder.errorLayout;
            this.emptyDataLayout = builder.emptyDataLayout;
    
            //創建幀佈局
            rootFrameLayout = new StateFrameLayout(this.context);
            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            rootFrameLayout.setLayoutParams(layoutParams);
    
            //設置狀態管理器
            rootFrameLayout.setStatusLayoutManager(this);
        }
    
    
        /**
         * 顯示loading
         */
        public void showLoading() {
            rootFrameLayout.showLoading();
        }
    
        /**
         * 顯示內容
         */
        public void showContent() {
            rootFrameLayout.showContent();
        }
    
        /**
         * 顯示空數據
         */
        public void showEmptyData(int iconImage, String textTip) {
            rootFrameLayout.showEmptyData(iconImage, textTip);
        }
    
        /**
         * 顯示空數據
         */
        public void showEmptyData() {
            showEmptyData(0, "");
        }
    
        /**
         * 顯示空數據
         */
        public void showLayoutEmptyData(Object... objects) {
            rootFrameLayout.showLayoutEmptyData(objects);
        }
    
        /**
         * 顯示網絡異常
         */
        public void showNetWorkError() {
            rootFrameLayout.showNetWorkError();
        }
    
        /**
         * 顯示異常
         */
        public void showError(int iconImage, String textTip) {
            rootFrameLayout.showError(iconImage, textTip);
        }
    
        /**
         * 顯示異常
         */
        public void showError() {
            showError(0, "");
        }
    
        public void showLayoutError(Object... objects) {
            rootFrameLayout.showLayoutError(objects);
        }
    
        /**
         * 得到root 佈局
         */
        public View getRootLayout() {
            return rootFrameLayout;
        }
    
        public static final class Builder {
    
            private Context context;
            private int loadingLayoutResId;
            private int contentLayoutResId;
            private ViewStub netWorkErrorVs;
            private int netWorkErrorRetryViewId;
            private ViewStub emptyDataVs;
            private int emptyDataRetryViewId;
            private ViewStub errorVs;
            private int errorRetryViewId;
            private int retryViewId;
            private int emptyDataIconImageId;
            private int emptyDataTextTipId;
            private int errorIconImageId;
            private int errorTextTipId;
            private AbsViewStubLayout errorLayout;
            private AbsViewStubLayout emptyDataLayout;
            private OnShowHideViewListener onShowHideViewListener;
            private OnRetryListener onRetryListener;
    
            Builder(Context context) {
                this.context = context;
            }
    
            /**
             * 自定義加載佈局
             */
            public Builder loadingView(@LayoutRes int loadingLayoutResId) {
                this.loadingLayoutResId = loadingLayoutResId;
                return this;
            }
    
            /**
             * 自定義網絡錯誤佈局
             */
            public Builder netWorkErrorView(@LayoutRes int newWorkErrorId) {
                netWorkErrorVs = new ViewStub(context);
                netWorkErrorVs.setLayoutResource(newWorkErrorId);
                return this;
            }
    
            /**
             * 自定義加載空數據佈局
             */
            public Builder emptyDataView(@LayoutRes int noDataViewId) {
                emptyDataVs = new ViewStub(context);
                emptyDataVs.setLayoutResource(noDataViewId);
                return this;
            }
    
            /**
             * 自定義加載錯誤佈局
             */
            public Builder errorView(@LayoutRes int errorViewId) {
                errorVs = new ViewStub(context);
                errorVs.setLayoutResource(errorViewId);
                return this;
            }
    
            /**
             * 自定義加載內容正常佈局
             */
            public Builder contentView(@LayoutRes int contentLayoutResId) {
                this.contentLayoutResId = contentLayoutResId;
                return this;
            }
    
            public Builder errorLayout(AbsViewStubLayout errorLayout) {
                this.errorLayout = errorLayout;
                this.errorVs = errorLayout.getLayoutVs();
                return this;
            }
    
            public Builder emptyDataLayout(AbsViewStubLayout emptyDataLayout) {
                this.emptyDataLayout = emptyDataLayout;
                this.emptyDataVs = emptyDataLayout.getLayoutVs();
                return this;
            }
    
            public Builder netWorkErrorRetryViewId(@LayoutRes int netWorkErrorRetryViewId) {
                this.netWorkErrorRetryViewId = netWorkErrorRetryViewId;
                return this;
            }
    
            public Builder emptyDataRetryViewId(@LayoutRes int emptyDataRetryViewId) {
                this.emptyDataRetryViewId = emptyDataRetryViewId;
                return this;
            }
    
            public Builder errorRetryViewId(@LayoutRes int errorRetryViewId) {
                this.errorRetryViewId = errorRetryViewId;
                return this;
            }
    
            public Builder retryViewId(@LayoutRes int retryViewId) {
                this.retryViewId = retryViewId;
                return this;
            }
    
            public Builder emptyDataIconImageId(@LayoutRes int emptyDataIconImageId) {
                this.emptyDataIconImageId = emptyDataIconImageId;
                return this;
            }
    
            public Builder emptyDataTextTipId(@LayoutRes int emptyDataTextTipId) {
                this.emptyDataTextTipId = emptyDataTextTipId;
                return this;
            }
    
            public Builder errorIconImageId(@LayoutRes int errorIconImageId) {
                this.errorIconImageId = errorIconImageId;
                return this;
            }
    
            public Builder errorTextTipId(@LayoutRes int errorTextTipId) {
                this.errorTextTipId = errorTextTipId;
                return this;
            }
    
            /**
             * 爲狀態View顯示隱藏監聽事件
             * @param listener                  listener
             * @return
             */
            public Builder onShowHideViewListener(OnShowHideViewListener listener) {
                this.onShowHideViewListener = listener;
                return this;
            }
    
            /**
             * 爲重試加載按鈕的監聽事件
             * @param onRetryListener           listener
             * @return
             */
            public Builder onRetryListener(OnRetryListener onRetryListener) {
                this.onRetryListener = onRetryListener;
                return this;
            }
    
            /**
             * 創建對象
             * @return
             */
            public StateLayoutManager build() {
                return new StateLayoutManager(this);
            }
        }
    
    }

5.3 如何管理多種狀態

  • 大約5種狀態,如何管理這些狀態?添加到集合中,Android中選用SparseArray比HashMap更省內存,在某些條件下性能更好,主要是因爲它避免了對key的自動裝箱(int轉爲Integer類型),它內部則是通過兩個數組來進行數據存儲的,一個存儲key,另外一個存儲value,爲了優化性能,它內部對數據還採取了壓縮的方式來表示稀疏數組的數據,從而節約內存空間

    /**存放佈局集合 */
    private SparseArray<View> layoutSparseArray = new SparseArray();
    
    /**將佈局添加到集合 */
    private void addLayoutResId(@LayoutRes int layoutResId, int id) {
        View resView = LayoutInflater.from(mStatusLayoutManager.context).inflate(layoutResId, null);
        layoutSparseArray.put(id, resView);
        addView(resView);
    }
    
    //那麼哪裏從集合中取數據呢
    public void showContent() {
        if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null) {
            showHideViewById(LAYOUT_CONTENT_ID);
        }
    }

06.封裝庫極致優化點說明

6.1 用ViewStub顯示佈局

  • 方法裏面通過id判斷來執行不同的代碼,首先判斷ViewStub是否爲空,如果爲空就代表沒有添加這個View就返回false,不爲空就加載View並且添加到集合當中,然後調用showHideViewById方法顯示隱藏View,retryLoad方法是給重試按鈕添加事件

    • 注意,即使當你設置了多種不同狀態視圖,調用setContentView的時候,因爲異常頁面使用ViewStub,所以在繪製的時候不會影響性能的。
    /**
    *  顯示loading
    */
    public void showLoading() {
        if (layoutSparseArray.get(LAYOUT_LOADING_ID) != null)
            showHideViewById(LAYOUT_LOADING_ID);
    }
    
    /**
    *  顯示內容
    */
    public void showContent() {
        if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null)
            showHideViewById(LAYOUT_CONTENT_ID);
    }
    
    //調用inflateLayout方法,方法返回true然後調用showHideViewById方法
    private boolean inflateLayout(int id) {
        boolean isShow = true;
        if (layoutSparseArray.get(id) != null) return isShow;
        switch (id) {
            case LAYOUT_NETWORK_ERROR_ID:
                if (mStatusLayoutManager.netWorkErrorVs != null) {
                    View view = mStatusLayoutManager.netWorkErrorVs.inflate();
                    retryLoad(view, mStatusLayoutManager.netWorkErrorRetryViewId);
                    layoutSparseArray.put(id, view);
                    isShow = true;
                } else {
                    isShow = false;
                }
                break;
            case LAYOUT_ERROR_ID:
                if (mStatusLayoutManager.errorVs != null) {
                    View view = mStatusLayoutManager.errorVs.inflate();
                    if (mStatusLayoutManager.errorLayout != null) mStatusLayoutManager.errorLayout.setView(view);
                    retryLoad(view, mStatusLayoutManager.errorRetryViewId);
                    layoutSparseArray.put(id, view);
                    isShow = true;
                } else {
                    isShow = false;
                }
                break;
            case LAYOUT_EMPTYDATA_ID:
                if (mStatusLayoutManager.emptyDataVs != null) {
                    View view = mStatusLayoutManager.emptyDataVs.inflate();
                    if (mStatusLayoutManager.emptyDataLayout != null) mStatusLayoutManager.emptyDataLayout.setView(view);
                    retryLoad(view, mStatusLayoutManager.emptyDataRetryViewId);
                    layoutSparseArray.put(id, view);
                    isShow = true;
                } else {
                    isShow = false;
                }
                break;
        }
        return isShow;
    }

6.2 處理重新加載邏輯

  • 最後看看重新加載方法

    /**
    *  重試加載
    */
    private void retryLoad(View view, int id) {
        View retryView = view.findViewById(mStatusLayoutManager.retryViewId != 0 ? mStatusLayoutManager.retryViewId : id);
        if (retryView == null || mStatusLayoutManager.onRetryListener == null) return;
        retryView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mStatusLayoutManager.onRetryListener.onRetry();
            }
        });
    }

07.如何使用該封裝庫

  • 可以自由切換內容,空數據,異常錯誤,加載,網絡錯誤等5種狀態。父類BaseActivity直接暴露5中狀態,方便子類統一管理狀態切換,這裏fragment的封裝和activity差不多。

    /**
    * ================================================
    * 作    者:楊充
    * 版    本:1.0
    * 創建日期:2017/7/6
    * 描    述:抽取類
    * 修訂歷史:
    * ================================================
    */
    public abstract class BaseActivity extends AppCompatActivity {
    
        protected StatusLayoutManager statusLayoutManager;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_base_view);
            initStatusLayout();
            initBaseView();
            initToolBar();
            initView();
        }
        
        //子類必須重寫該方法
        protected abstract void initStatusLayout();
    
        protected abstract void initView();
    
        /**
        * 獲取到佈局
        */
        private void initBaseView() {
            LinearLayout ll_main = (LinearLayout) findViewById(R.id.ll_main);
            ll_main.addView(statusLayoutManager.getRootLayout());
        }
    
        //正常展示數據狀態
        protected void showContent() {
            statusLayoutManager.showContent();
        }
    
        //加載數據爲空時狀態
        protected void showEmptyData() {
            statusLayoutManager.showEmptyData();
        }
    
        //加載數據錯誤時狀態
        protected void showError() {
            statusLayoutManager.showError();
        }
    
        //網絡錯誤時狀態
        protected void showNetWorkError() {
            statusLayoutManager.showNetWorkError();
        }
    
        //正在加載中狀態
        protected void showLoading() {
            statusLayoutManager.showLoading();
        }
    }
  • 子類繼承BaseActivity後,該如何操作呢?具體如下所示

    @Override
    protected void initStatusLayout() {
        statusLayoutManager = StateLayoutManager.newBuilder(this)
                .contentView(R.layout.activity_main)
                .emptyDataView(R.layout.activity_emptydata)
                .errorView(R.layout.activity_error)
                .loadingView(R.layout.activity_loading)
                .netWorkErrorView(R.layout.activity_networkerror)
                .build();
    }
    
    //或者添加上監聽事件
    @Override
    protected void initStatusLayout() {
        statusLayoutManager = StateLayoutManager.newBuilder(this)
                .contentView(R.layout.activity_content_data)
                .emptyDataView(R.layout.activity_empty_data)
                .errorView(R.layout.activity_error_data)
                .loadingView(R.layout.activity_loading_data)
                .netWorkErrorView(R.layout.activity_networkerror)
                .onRetryListener(new OnRetryListener() {
                    @Override
                    public void onRetry() {
                        //爲重試加載按鈕的監聽事件
                    }
                })
                .onShowHideViewListener(new OnShowHideViewListener() {
                    @Override
                    public void onShowView(View view, int id) {
                        //爲狀態View顯示監聽事件
                    }
    
                    @Override
                    public void onHideView(View view, int id) {
                        //爲狀態View隱藏監聽事件
                    }
                })
                .build();
    }
    
    //如何切換狀態呢?
    showContent();
    showEmptyData();
    showError();
    showLoading();
    showNetWorkError();
    
    //或者這樣操作也可以
    statusLayoutManager.showLoading();
    statusLayoutManager.showContent();
  • 那麼如何設置狀態頁面的交互事件呢?當狀態是加載數據失敗時,點擊可以刷新數據;當狀態是無網絡時,點擊可以設置網絡。代碼如下所示:

    /**
    * 點擊重新刷新
    */
    private void initErrorDataView() {
        statusLayoutManager.showError();
        LinearLayout ll_error_data = (LinearLayout) findViewById(R.id.ll_error_data);
        ll_error_data.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                initData();
                adapter.notifyDataSetChanged();
                showContent();
            }
        });
    }
    
    /**
    * 點擊設置網絡
    */
    private void initSettingNetwork() {
        statusLayoutManager.showNetWorkError();
        LinearLayout ll_set_network = (LinearLayout) findViewById(R.id.ll_set_network);
        ll_set_network.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent("android.settings.WIRELESS_SETTINGS");
                startActivity(intent);
            }
        });
    }
  • 那有些頁面想要自定義指定的狀態頁面UI,又該如何操作呢?倘若有些頁面想定製狀態佈局,也可以自由實現,很簡單:

    /**
    * 自定義加載數據爲空時的狀態佈局
    */
    private void initEmptyDataView() {
        statusLayoutManager.showEmptyData();
        //此處是自己定義的狀態佈局
        statusLayoutManager.showLayoutEmptyData(R.layout.activity_emptydata);
        LinearLayout ll_empty_data = (LinearLayout) findViewById(R.id.ll_empty_data);
        ll_empty_data.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                initData();
                adapter.notifyDataSetChanged();
                showContent();
            }
        });
    }

其他介紹

01.關於博客彙總鏈接

02.關於我的博客

項目開源地址:https://github.com/yangchong211/YCStateLayout

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