淺談BaseActivity的進階寫法,基礎框架的搭建,促使我們更高效便捷開發

@TOC.

淺談BaseActivity的進階寫法,基礎框架的搭建,促使我們更高效便捷開發

大家好!時隔許久沒寫過博客了,最近因疫情原因導致更換了公司,接收公司的項目代碼,AB端外加兩個基礎打印項目共4個,4個項目用了3個不同的網絡訪問風格和代碼風格,最讓我哭笑不得的是隻有一個界面的打印項目還運用的 MVP 寫法,在看項目base基本無封裝,無共用代碼塊,再改了幾個需求終覺得不可忍受,就有了這次的基礎框架搭建。(當然老菜鳥能力有限,大神請輕噴)

先看看現在項目的base吧

A端

public class BaseActivity extends RxAppCompatActivity {
    private static final String TAG = "BaseActivity";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {
            //Util.disabledDisplayDpiChange(this);
        } catch (Exception e) {
            Log.e(TAG, "disabledDisplayDpiChange failed!", e);
        }
    }
}

B端

public abstract class BaseActivity extends RxFragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getContentViewId());
        ButterKnife.bind(this);
        getWindow().setBackgroundDrawable(null);
        init();
    }

    protected abstract void init();

    protected abstract int getContentViewId();
}

看完是不是內心有種媽賣批的衝動,想改個title圖標都得單個單個去修改,而且不同的界面Activity裏相同代碼一堆,簡直無複用,要改某一個方法就得每個界面去修改,我太難了!說好的封裝呢?

思考一下?

一個BaseActivity需要些什麼呢?一般我們都會根據項目需求,原型來配置,當然也逃不掉一套基礎配置,對於代碼編寫,每個人都有每個人的風格,這裏就說說我的看法(輕噴)。以前我都是從封裝共用代碼的角度出發來搭建BaseActivity的,到後來我想精簡我們每個功能性界面的時候才醒悟過來,從結果角度出發,思考怎麼精簡我們的業務邏輯界面,最好的一鍵生成,傻瓜式編寫,這樣也是不錯的角度,通常業務界面需要的功能有哪些呢?我把他分爲5類(分類根據個人和項目實際情況)

1、只需要提交數據的界面:如修改密碼,暱稱之類
2、 只需要拉取數據的非列表界面:如查看個人資料,關於我們之類
3、既需要拉取數據又要提交數據界面。
4、列表界面(加頭部加尾部,上拉加載下拉刷新)
5、複雜多列表界面(很多商城類APP首頁都是)

那麼我們應該怎麼精簡這些界面呢?我例舉一個界面最少需要的東西(最少哈,業務不同的自行添加),順序對應上面的5類

界面的Title,界面的xml,Url鏈接 這都是必須要的
1、 +提交的參數,提交完成的邏輯處理
2、+拉取數據成功的邏輯處理
3、+兩個Url鏈接,拉取數據成功的邏輯處理,提交的參數,提交完成的邏輯處理
4、+界面的xml變item的,item的數據處理和業務邏輯處理,是否開啓上拉加載,是否添加頭尾等
5、 +內容xml+url鏈接(可能多個)+Binder的創建+獲取數據源之後的邏輯處理

上面簡單列一下用到的框架,不然有些看不懂

butterknife:用來實例化控件
retrofit2+rxjava2+rxlifecycle2+okhttp3+rxbinding3+rxpermissions:一套
bga-baseadapter:RecyclerView.Adapter的封裝
multitype:RecyclerView 輔助 多列表,主要用於4.5
kprogresshud:加載框
SmartRefreshLayout,SmartRefreshHeader:上拉下滑 刷新佈局的使用

下面看一下我根據上面的思想初步封裝的base的使用

public class TestUpDataActivity extends BaseUpDataActivity {

    @BindView(R.id.test_tv)
    TextView testTv;
    private String A, B;

    @Override
    protected String getTitles() {
        return "提交數據用例";
    }

    @Override
    public int getContentLayoutId() {
        return R.layout.activity_test;
    }

    @Override
    public String getUrl() {
        return Urls.URL_VERSION;
    }

    @Override
    public Map getMapHelper() {
        return new MapHelper().param("AAA", A).param("BBB", B).build();
    }

    @Override
    public void initListener() {
        super.initListener();
        A = "aaa";
        B = "bbb";
    RxView.clicks(testTv).throttleFirst(Constant.CLICK_DELAY_TIME, TimeUnit.MILLISECONDS)
                .compose(bindToLifecycle())
                .subscribe(unit ->  intoHttp(TestUpDataActivity.this));
    }
}

測試用例:第1類,可以看到我的activity只有5個方法,是不是簡單明瞭(由於是測試用例,所以寫的比較簡便但不影響閱讀),(界面的Title,界面的xml,Url鏈接,提交的參數,提交完成的邏輯處理)正好就是這5個方法就完成了一個簡單界面,當然getTitles,getUrl,getMapHelper,也可以換多種形式編寫,例如base裏定義變量,在提交之前賦值或者base定義方法set賦值,個人覺得這種不易被忽視忘記,並且方法自動生成,下面大家可能比較好奇intoHttp(),我們進去看一下

public abstract class BaseUpDataActivity<T> extends BaseHeaderActivity {

    public T t;
    
    public void intoHttp() {
        HttpClient.getInstance().doPostWithToken(this, getUrl(), getMapHelper(), new CallbackObserver<T>() {
            @Override
            public void onSuccess(BaseResponse<T> httpResult) {
                initSuccess(httpResult);
            }
        });
    }

    public void intoHttp(BaseActivity context) {
        HttpClient.getInstance().doPostWithToken(this, getUrl(), getMapHelper(), new CallbackObserver<T>(context) {
            @Override
            public void onSuccess(BaseResponse<T> httpResult) {
                initSuccess(httpResult);
            }
        });
    }
    
    public void initSuccess(BaseResponse<T> response) {
        ToastUtil.showToastSuccess(response.getMsg());
        Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        if (type != null) {
            t = GsonUtils.fromJson(GsonUtils.toJson(response.data), type);
        }
        initRefreshView();
    }

    /**
     * 默認提交數據完成的操作
     */
    public void initRefreshView() {
        toFinish();
    }
    
    public abstract String getUrl();

    public abstract Map getMapHelper();

}

可以看到,我在原本的base之上在加了一層,該層主要是爲了精簡訪問網絡,處理訪問網絡之後的默認代碼,再在base裏面就是大家所熟知的基礎封裝了,代碼就不列了,簡單說明一下

1、屏幕橫豎屏切換,AndroidManifest中將不用再寫android:screenOrientation=“portrait”(之前看到過一篇博客分析,推薦Manifest設置更優,但是要每個添加)
2、ButterKnife綁定頁面和解綁
3、沉浸式實現以及頂部圖片頂置等
4、activity普通跳轉,攜帶數據的頁面跳轉
5、返傳值跳轉及setResult
5、返回鍵時間監聽
6、標題欄統一實現(是否需要標題欄等)
7、仿IOS側滑finish頁面實現(繼承SwipeBackActivity),默認關閉動畫等,
8、Loading頁面統一實現(kprogresshud)
9、AppManager管理類的add及remove
10、數據展示頁面的State(各種狀態自動封裝,如無網絡,無數據,服務器錯誤等)

基本上就是這些,當然一些跟項目相關的東西我就不例舉了,下面看看第2類的界面代碼吧

public class TestDataActivity extends BaseDataActivity<UpdateVersionBean> {

    @BindView(R.id.test_tv)
    TextView testTv;

    @Override
    protected String getTitles() {
        return "獲取服務器數據展示用例";
    }

    @Override
    public int getContentLayoutId() {
        return R.layout.activity_test;
    }

    @Override
    public String getUrl() {
        return Urls.URL_VERSION;
    }

    @Override
    public void initRefreshView() {
        testTv.setText(t.getDescribes());
    }
}

測試用例:第2類,可以看到我的activity只有4個方法(由於是測試用例,所以寫的比較簡便但不影響閱讀),(界面的Title,界面的xml,Url鏈接,讀取到數據之後的界面邏輯處理)正好就是這4個方法就完成了一個簡單界面,當然getTitles,getUrl,跟上面講的一樣,實習方法多樣化,但是的注意先賦值在訪問,當然有些可以需要參數,那麼像第一類一樣加進去就好了,下面我們來看看他繼承的類。

public abstract class BaseDataActivity<T> extends BaseHeaderActivity {

    public T t;

    @Override
    public void initData() {
        super.initData();
        if (isIntoHttp())
            intoHttp(this);
    }

    public void intoHttp() {
        HttpClient.getInstance().doGetWithToken(this, getUrl(), new CallbackObserver<T>() {
            @Override
            public void onSuccess(BaseResponse<T> httpResult) {
                initSuccess(httpResult);
            }
        });
    }

    public void intoHttp(BaseActivity context) {
        HttpClient.getInstance().doGetWithToken(this, getUrl(), new CallbackObserver<T>(context) {
            @Override
            public void onSuccess(BaseResponse<T> httpResult) {
                initSuccess(httpResult);
            }
        });
    }

    public abstract String getUrl();

    public abstract void initRefreshView();

    /**
     * 是否進來就加載
     *
     * @return
     */
    public boolean isIntoHttp() {
        return true;
    }

    public void initSuccess(BaseResponse<T> response) {
            Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            t = GsonUtils.toJsonBean(response.data, type);
            if (t!=null){
                initRefreshView();
            }
    }
}

跟之前的差不多,該層也主要是爲了精簡訪問網絡,處理訪問網絡數據的默認代碼,由於代碼比較簡單就沒加什麼註釋,再在base裏面就是上面所講的那些了,這裏不做解釋。可以大家比較期待的還是列表類,複雜列表類,因爲這些纔是正真方便很多的,好了第四類我們就不說講,基本跟1.2類似,我們之間跳到第4類。

列表類該如何封裝

以前每次實現列表類的界面,都需要從之前寫的列表界面拷貝許多代碼過來,就會比較煩,後來我就想着要把這些代碼都抽取出來封裝好,我們先來想想實現一個列表界面最少最少需要幾個方法

1、標題:setTitles,
2、item內容xml
3、拉取數據的url
4、獲取服務器數據的參數
5、adapter裏的item數據處理和代碼邏輯

來看下我封裝之後的簡單列表實現吧,默認開啓上拉下滑功能

public class TestItemActivity extends BaseItemActivity<TestListBean> {
    public int pageNo,pageSize;

    @Override
    protected String getTitles() {
        return "測試list";
    }

    @Override
    public int getItemLayoutId() {
        return R.layout.activity_item_test;
    }

    @Override
    public String getUrl() {
        return Urls.testList;
    }

    @Override
    public Map getMapHelper() {
        return new MapHelper().param("pageNo",pageNo+++"").param("pageSize",pageSize+"").param("AAA", "").param("BBB", "").build();
    }

    @Override
    public void itemCover(BGAViewHolderHelper helper, int position, TestListBean model) {
        helper.setText(R.id.item_test_tv, model.getGoodsName());
    }
}

測試用例:第4類,整好就是我說的那五個最少的方法,是不是覺得好簡單,從此之後媽媽再也不用擔心我寫列表界面繁瑣了,(由於是測試用例,所以寫的比較簡便但不影響閱讀),getTitles,getUrl,getMapHelper,依舊是可以按照自己的寫法搞,下面給大家看看BaseItemActivity裏面的代碼封裝(由於更換了網絡框架,之前用的okgo,尚未正式使用,可能在一些判斷上還存在問題)

BaseItemActivity代碼

public abstract class BaseItemActivity<T> extends BaseStateActivity implements AdapterCoverHelper<T> {

    @BindView(R.id.mRecyclerView)
    public RecyclerView mRecyclerView;
    @BindView(R.id.mNestedScrollView)
    public NestedScrollView mNestedScrollView;
    @BindView(R.id.refreshLayout)
    public SmartRefreshLayout refreshLayout;

    public List<T> items = new ArrayList<>();
    public BaseItemAdapter<T> baseItemAdapter;
    public int pageNo=1,pageSize=20;
    @Override
    public int getContentLayoutId() {
        return R.layout.activity_baseitem;
    }

    @Override
    protected void initStateRefresh() {
        setState(Constant.STATE_LOADING);
    }

    @Override
    protected void initView() {
        initRecyclerView();
        initAdapterView();
    }

    public void initRecyclerView() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        if (isDividerItemDecoration()) {
            DividerItemDecoration divider = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
            divider.setDrawable(ContextCompat.getDrawable(this, R.drawable.recommend_item_divider));
            mRecyclerView.addItemDecoration(divider);
        }
        mRecyclerView.setFocusable(false);
    }

    public void initAdapterView() {
        baseItemAdapter = new BaseItemAdapter(mRecyclerView, getItemLayoutId(), this);
        baseItemAdapter.setData(items);
        baseItemAdapter.notifyDataSetChanged();
        if (getHeaderView() != null) baseItemAdapter.addHeaderView(getHeaderView());
        if (addFooterView() != null) baseItemAdapter.addFooterView(addFooterView());
        mRecyclerView.setAdapter(baseItemAdapter);
    }

    @Override
    public void initListener() {
        refreshLayout.setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh(RefreshLayout refreshlayout) {
                isOpenLoadMore(true);
                pageNo=1;
                items.clear();
                initHttpData();
            }
        });
        refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() {
            @Override
            public void onLoadMore(RefreshLayout refreshlayout) {
                initHttpData();
            }
        });
    }

    @Override
    public void initHttpData() {
        if (isIntoHttp())
            HttpClient.getInstance().doGetWithToken(this, getUrl(),getMapHelper(), new DisposableObserver<ResponseBody>() {
                @Override
                public void onNext(ResponseBody t) {
                    try {
                        convert(t);
                    } catch (IOException e) {
                        e.printStackTrace();
                        initErrorFinishRefreshAndLoadMore();
                    }
                }
                @Override
                public void onError(Throwable e) {
                    initError(e);
                }

                @Override
                public void onComplete() {

                }
            });
    }

    public void convert(ResponseBody value) throws IOException {
        Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        //Class<T> tClass = (Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        Type types = new TypeToken<BaseResponse<JsonArray>>() {
        }.getType();
        BaseResponse<JsonArray> baseResponse = GsonUtils.fromJson(value.string(), types);
        List<T> list = GsonUtils.getObjectList(baseResponse.data, type);
        value.close();
        if (baseResponse.status == 1) {
            initSuccess(list);
        } else {
            if (!isSize(baseItemAdapter.getData()))
            initSetState(Constant.STATE_ERROR);
            initErrorFinishRefreshAndLoadMore();
        }
    }

    public abstract int getItemLayoutId();

    public abstract String getUrl();
    public abstract Map getMapHelper();
    public boolean isDividerItemDecoration() {
        return true;
    }

    /**
     * 是否進來就加載
     *
     * @return
     */
    public boolean isIntoHttp() {
        return true;
    }

    public View getHeaderView() {
        return null;
    }

    public View addFooterView() {
        return null;
    }

    public void initSuccess(BaseResponse<List<T>> response) {
        if (response.status == 1) {
            if (isSize(response.data)) {
                if (!flStateContent.isSuccess())
                initSetState(Constant.STATE_SUCCESS);
                initSuccessFinishRefreshAndLoadMore();
                items.addAll(response.data);
                baseItemAdapter.setData(items);
            } else {
                if (items.size()==0)
                initSetState(Constant.STATE_EMPTY);
                initErrorFinishRefreshAndLoadMore();
                isOpenLoadMore(false);
            }
        } else {
            initSetState(Constant.STATE_ERROR);
            initErrorFinishRefreshAndLoadMore();
        }
        //if (refreshLayout == null) return;
        //refreshLayout.computeScroll();
    }

    public void initSuccess(List<T> response) {
        if (isSize(response)) {
            if (!flStateContent.isSuccess())
            initSetState(Constant.STATE_SUCCESS);
            initSuccessFinishRefreshAndLoadMore();
            items.addAll(response);
            baseItemAdapter.setData(items);
        } else {
            if (items.size()==0)
            initSetState(Constant.STATE_EMPTY);
            initErrorFinishRefreshAndLoadMore();
            isOpenLoadMore(false);
        }
        //if (refreshLayout == null) return;
        //refreshLayout.computeScroll();
    }

    public void initError(Throwable response) {
        initSetState(Constant.STATE_ERROR);
        ToastUtil.showToastError(response.getMessage());
        initErrorFinishRefreshAndLoadMore();
    }

    public void initSetState(int state) {
        if (flStateContent.isSuccess()){
            if (!isSize(baseItemAdapter.getData()))
                setState(state);
        }else {
            setState(state);
        }
    }

    /**
     * 成功拉取服務器數據,刷新拉取時間
     *
     * @param isRefresh true 下拉刷新結束 false 上拉加載結束
     */
    private void initSuccessFinishRefreshAndLoadMore(boolean isRefresh) {
        if (refreshLayout == null) return;
        if (isRefresh) {
            refreshLayout.finishRefresh(true);
        } else {
            refreshLayout.finishLoadMore(true);
        }
    }

    /**
     * 訪問服務器失敗,不刷新拉取時間
     *
     * @param isRefresh true 下拉刷新結束 false 上拉加載結束
     */
    private void initErrorFinishRefreshAndLoadMore(boolean isRefresh) {
        if (refreshLayout == null) return;
        if (isRefresh) {
            refreshLayout.finishRefresh(false);
        } else {
            refreshLayout.finishLoadMore(false);
        }
    }

    /**
     * 是否開啓上拉加載,未滿一屏則默認不可下拉加載
     *
     * @param isOpen
     */
    private void isOpenLoadMore(boolean isOpen) {
        if (refreshLayout == null) return;
        refreshLayout.setEnableLoadMore(isOpen);
    }

    private void initErrorFinishRefreshAndLoadMore() {
        if (refreshLayout == null) return;
        switch (refreshLayout.getState()) {
            case Refreshing:
                refreshLayout.finishRefresh(false);
                break;
            case Loading:
                refreshLayout.finishLoadMore(false);
                break;
            case None:
                break;
            default:
                refreshLayout.setNoMoreData(false);
        }
    }

    private void initSuccessFinishRefreshAndLoadMore() {
        if (refreshLayout == null) return;
        switch (refreshLayout.getState()) {
            case Refreshing:
                refreshLayout.finishRefresh(true);
                break;
            case Loading:
                refreshLayout.finishLoadMore(true);
                break;
            case None:
                break;
            default:
                refreshLayout.setNoMoreData(true);
        }
    }
}

BaseItemAdapter代碼

public class BaseItemAdapter<T> extends BGARecyclerViewAdapter<T> {

    private AdapterCoverHelper<T> coverHelper;
    public BaseItemAdapter(RecyclerView recyclerView, int itemLayoutId, AdapterCoverHelper<T> coverHelper) {
        super(recyclerView, itemLayoutId);
        this.coverHelper = coverHelper;
    }

    @Override
    protected void fillData(BGAViewHolderHelper helper, int position, T model) {
        coverHelper.itemCover(helper, position, model);
    }
}

AdapterCoverHelper代碼

public interface AdapterCoverHelper<T> {
    void itemCover(BGAViewHolderHelper helper, int position, T model);
}

BaseItemActivity引用的xml

<com.scwang.smartrefresh.layout.SmartRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:zhy="http://schemas.android.com/apk/res-auto"
    android:id="@+id/refreshLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/mNestedScrollView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/mRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:nestedScrollingEnabled="false" />
    </androidx.core.widget.NestedScrollView>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>

BaseStateActivity代碼是我在BaseActivity基礎上加了狀態界面的封裝代碼,這裏就不展示了,比較簡單(狀態:加載中,空界面,錯誤界面,無網絡界面)

可以看到,我把RecyclerView,SmartRefreshLayout的默認配置,以及數據的獲取,解析,和對網絡獲取數據成功失敗的判斷 操作都放在了BaseItemActivity中,大大減少了我們在編寫列表是所需要的代碼,本來數據解析是可以更簡單一些的,可以由於泛型傳達的擦除,導致需要重新解析一次,不過還是可以,畢竟以後書寫的代碼量少很香的,邏輯也相當的清晰,哈哈。

結尾

由於篇幅限制,第五種複雜列表封裝在這裏就先不記錄了,畢竟用到的也比較少,一般主頁大家也都不會用多列表來實現,而會選擇碎片化的fragment,我也是因爲項目需要自己寫一個APP聊天系統,需要寫聊天界面,所以才封裝了這麼一個複雜列表實現基類,下次有時間在記錄吧。

老菜鳥水平不高,有大神瀏覽到還希望多給給意見,指出指出錯誤哈,不勝感激。

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