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

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

一、復雜視圖分析

首先來看兩個圖,這是小米游戲的詳情頁,將圖1上滑得到圖2

圖1

圖2

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

UI結構分析

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

視圖大致拆分

二、實現

我們使用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,現在看如何使用它,見證一下效果。

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再次分發給內部那些真正需要數據的模塊。對于視圖業務模塊顯得簡單直白,結構良好,因為這一步劃分了邊界,使得頁面結構的明晰化。我使用這種思想的難點是視圖切片的粒度劃分,太細需要寫很多代碼,太少達不到效果,這個需要根據需求來把握,思想是活的,模式之間相互變通,才能寫出良好的系統結構,減少開發維護成本。

 

 

來自:http://www.jianshu.com/p/4338e4530024

 

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