@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聊天系統,需要寫聊天界面,所以才封裝了這麼一個複雜列表實現基類,下次有時間在記錄吧。
老菜鳥水平不高,有大神瀏覽到還希望多給給意見,指出指出錯誤哈,不勝感激。