Android 架構設計 --- 關於 View 邊界劃分的思考

Looperjing發表於2017-03-22

版權宣告:本文為LooperJing原創文章,未經博主允許不得轉載!

在前幾篇,我總結了MVP,MVVM,對MVP使用泛型,以避免類爆炸,這些方案的實施在一定的程度的,使得View和業務邏輯成功隔離開來,但是對於一個複雜的介面,,一個layout.xml即使使用了和自定義控制元件,上千行也是很有可能的。所以這篇部落格,主要記錄業務檢視模組怎麼編寫比較好,當然這不是教科書,只是分享我關於這方面的思考。

###一、複雜檢視分析
首先來看兩個圖,這是小米遊戲的詳情頁,將圖1上滑得到圖2

Android 架構設計 --- 關於 View 邊界劃分的思考
圖1

Android 架構設計 --- 關於 View 邊界劃分的思考
圖2

看上去,這個頁面很複雜了,分析一下UI結構,不全部展開,大概分成五個部分,最上方的三個TAB(介紹,評論,周邊)、遊戲滑動宣傳圖Banner、遊戲活動、遊戲推薦位、遊戲安裝。

Android 架構設計 --- 關於 View 邊界劃分的思考
UI結構分析

試想,如果這個所有的子業務邏輯都寫在頁面載體中,從資料獲取,檢視設定、錯誤處理,互動跳轉,那麼這個Activity體量是很龐大的,維護相當困難。若你使用合理的架構將業務邏輯與檢視控制解耦,Activity體量確實明顯降低,但是體量還是很大,在JAVA的編碼規範中,每個類不能長於1000行,一個方法的長度儘量控制在50行,1000很容易就超過了。所以對於複雜的問題,"Divide and Conquer"(分而治之)的思想屢試不爽,按照業務功能級進行拆分,就得到複數個的檢視切片,於是在檢視層和業務邏輯層之間產生了以子業務為粒度的對映,業務檢視模組封裝了這類對映,每個模組封裝了特定子業務的檢視切片配置和業務邏輯。根據這種思想我將這個複雜的檢視分成4個部分,如下圖。

Android 架構設計 --- 關於 View 邊界劃分的思考
檢視大致拆分

###二、實現
我們使用Holder來管理檢視,對於每一個檢視,基本功能具有獲取資料、重新整理資料、設定資料、findViewById、返回整個檢視給外部使用等,所以我寫出以下的基類。

public abstract class ViewBaseHolder<T> {

    private T mBaseData;
    private Context mContext;
    private View mRootView;

    public ViewBaseHolder(Context pContext) {
        this(pContext, null, 0);
    }

    public ViewBaseHolder(Context pContext, ViewGroup pViewParent) {
        this(pContext, pViewParent, 0);
    }

    public ViewBaseHolder(Context pContext, ViewGroup pViewParent, int pResId) {
        this(pContext, pViewParent, pResId == 0 ? null : LayoutInflater.from(pContext).inflate(pResId, pViewParent, false));
    }

    public ViewBaseHolder(Context pContext, ViewGroup pViewParent, View pRootView) {
        mContext = pContext;
        mRootView = initView(pViewParent, pRootView);
        initEvent(mRootView);
    }

    /**
     * 獲取資料
     *
     * @return
     */
    public T getData() {
        return mBaseData;
    }

    /**
     * 設定資料
     * @param pData
     */
    public void setDateAndRefreshView(T pData) {
        mBaseData = pData;
        refreshView(pData);
    }

    /**
     * 用來通知重新整理資料
     */
    public void notifyDataSetChange() {
        refreshView(getData());
    }

    /**
     * 用來重新整理資料
     */
    public abstract void refreshView(T pData);

     /**
     *findViewById
     */
    public abstract View initView(ViewGroup pViewParent, View pRootView);


     /**
      返回整個View給外部
    */
    public View getRootView() {
        return mRootView;
    }


    public void initEvent(View pBaseRootView) {}


    public Context getContext() {
        return mContext;
    }

    public Activity getActivity() {
        if (mContext instanceof Activity) {
            return (Activity) mContext;
        }
        return null;
    }
}複製程式碼

對與遊戲推薦這個檢視切片,用DetailRecommendHoler 來管理,需要實現上面的ViewBaseHolder,考慮到互動跳轉的時候,需要伺服器得到某些資訊,把Activity中的Presenter傳進來了。DetailRecommendHoler 需要設定檢視資訊,給外部返回檢視根View,重新整理檢視,設定監聽事件等等。

public class DetailRecommendHoler extends ViewBaseHolder<GameEntry> {

    private GameDetailPresenter mPresenter;


    private TextView mGameName;

    private ImageView mGameIcno;

    public DetailRecommendHoler(Context pContext, ViewGroup pViewParent, GameDetailPresenter pPresenter) {
        super(pContext, pViewParent);
        mPresenter = pPresenter;
    }

    @Override
    public void initEvent(View pBaseRootView) {
        super.initEvent(pBaseRootView);
        mGameIcno.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //mPresenter.toDownLoadGameActivity(getContext());
            }
        });
    }

    public DetailRecommendHoler(Context pContext, ViewGroup pViewParent, int pResId, GameDetailPresenter pPresenter) {
        super(pContext, pViewParent, pResId);
        mPresenter = pPresenter;
    }

    @Override
    public void refreshView(GameEntry pData) {
        mGameIcno.setImageResource(pData.url);
        mGameName.setText(pData.name);
    }

    @Override
    public View initView(ViewGroup pViewParent, View pRootView) {
        View rootView;
        if (pRootView == null) {
            rootView = LayoutInflater.from(getContext()).inflate(R.layout.game_recommend_holder, null);
        } else {
            rootView = pRootView;
        }
        mGameIcno = (ImageView) pRootView.findViewById(R.id.game_inco);
        mGameName = (TextView) pRootView.findViewById(R.id.game_name);
        return rootView;
    }

    public GameDetailPresenter getPresenter() {
        return mPresenter;
    }
}複製程式碼

同理對於遊戲詳情中1檢視切片可以用DetailBannerHoler來管理,對於遊戲詳情中2檢視切片可以用DetailActivityHoler來管理,對於遊戲詳情中4檢視切片可以用DetailDownLoadHoler來管理。通過這樣的分而治之,完全可以避免一個頁面太重的影響,一個檢視切片程式碼量少,維護起來很方便,比如對於上面的的3切片,如果有其他模組需要使用,直接使用即可。相應的,經過“分而治之”之後,layout也分成了幾個部分,如下。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <FrameLayout
        android:id="@+id/game_banner_container"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginTop="60dp"
     />

    <FrameLayout
        android:id="@+id/game_activity_container"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_below="@id/game_banner_container"
        />

    <FrameLayout
        android:id="@+id/game_recomend_container"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_below="@+id/game_activity_container"
     />

    <FrameLayout
        android:id="@+id/game_download_container"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_below="@+id/game_recomend_container"
       />

</RelativeLayout>複製程式碼

OK,現在看如何使用它,見證一下效果。下面有太不明白的,需要了解一下,MVP(戳我)[www.jianshu.com/p/3a17382d4…]


public class GameDetailActivity extends BaseActivity<GameDetailPresenter, GameDtailModel> implements GameContract.GameDetailView {


    private FrameLayout mGameRecommendFly;

    private FrameLayout mGameActivityFly;

    private FrameLayout mGameDownLoadFly;

    private FrameLayout mGameBannerFly;

    private DetailRecommendHoler mDetailRecommendHoler;

    private DetailBannerHoler mDetailBannerHoler;

    private ProgressBar mLoadingBar;

    private ListView mListView;

    @Override
    public int getLayoutResId() {
        return R.layout.activity_main;
    }

    @Override
    public void initView() {

        mPresenter.requestGameEntry(1, 1);

        mGameRecommendFly = (FrameLayout) findViewById(R.id.game_recomend_container);
        mDetailRecommendHoler = new DetailRecommendHoler(this, mGameRecommendFly, mPresenter);
        mGameRecommendFly.addView(mDetailRecommendHoler.getRootView());

        mGameBannerFly = (FrameLayout) findViewById(R.id.game_banner_container);
        mDetailRecommendHoler = new DetailBannerHoler(this, mGameBannerFly, mPresenter);
        mGameBannerFly.addView(mDetailBannerHoler.getRootView());

        ...
    }


    @Override
    public void showLoading() {
        mLoadingBar.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        mLoadingBar.setVisibility(View.INVISIBLE);
    }

    @Override
    public void showError() {
        TextView errorView = new TextView(this);
        errorView.setTextSize(20);
        errorView.setText("請求失敗了");
        mListView.setEmptyView(errorView);
    }


    @Override
    public void setGameEntry(GameEntry pGameEntry) {

        mDetailRecommendHoler.refreshView(pGameEntry.listone);
        mDetailRecommendHoler.refreshView(pGameEntry.listtwo);
        .......

    }
}複製程式碼

這種寫法,Activity/Fragment本身不再承載任何檢視業務邏輯,僅僅需要維護內部寄生的模組Activity/Fragment的職責退化為模組容器和資料媒介,資料媒介的作用體現在,Activity/Fragment在某些情況下會扮演頁面資料的入口和分發者,Data到達Activity後,Activity要將Data再次分發給內部那些真正需要資料的模組。對於檢視業務模組顯得簡單直白,結構良好,因為這一步劃分了邊界,使得頁面結構的明晰化。我使用這種思想的難點是檢視切片的粒度劃分,太細需要寫很多程式碼,太少達不到效果,這個需要根據需求來把握,思想是活的,模式之間相互變通,才能寫出良好的系統結構,減少開發維護成本。

推薦閱讀:
Android架構設計---MVP模式第(一)篇之基本認實
Android架構設計---MVP模式第(二)篇,如何減少類爆炸
Android架構設計---關於MVVM模式的探討

Please accept mybest wishes for your happiness and success !

相關文章