@TOC.
大家好!时隔许久没写过博客了,最近因疫情原因导致更换了公司,接收公司的项目代码,AB端外加两个基础打印项目共4个,4个项目用了3个不同的网络访问风格和代码风格,最让我哭笑不得的是只有一个界面的打印项目还运用的 MVP 写法,在看项目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:上拉下滑 刷新布局的使用
单纯的提交数据用例
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(),我们进去看一下
单纯的提交数据用例Base
public abstract class BaseUpDataActivity extends BaseHeaderActivity {
public T t;
public void intoHttp() {
HttpClient.getInstance().doPostWithToken(this, getUrl(), getMapHelper(), new CallbackObserver() {
@Override
public void onSuccess(BaseResponse httpResult) {
initSuccess(httpResult);
}
});
}
public void intoHttp(BaseActivity context) {
HttpClient.getInstance().doPostWithToken(this, getUrl(), getMapHelper(), new CallbackObserver(context) {
@Override
public void onSuccess(BaseResponse httpResult) {
initSuccess(httpResult);
}
});
}
public void initSuccess(BaseResponse 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 TestGetDataActivity extends BaseDataActivity {
@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,跟上面讲的一样,实习方法多样化,但是的注意先赋值在访问,当然有些可以需要参数,那么像第一类一样加进去就好了,下面我们来看看他继承的类。
单纯的获取服务器数据展示用例的base
public abstract class BaseGetDataActivity 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() {
@Override
public void onSuccess(BaseResponse httpResult) {
initSuccess(httpResult);
}
});
}
public void intoHttp(BaseActivity context) {
HttpClient.getInstance().doGetWithToken(this, getUrl(), new CallbackObserver(context) {
@Override
public void onSuccess(BaseResponse httpResult) {
initSuccess(httpResult);
}
});
}
public abstract String getUrl();
public abstract void initRefreshView();
/**
* 是否进来就加载
*
* @return
*/
public boolean isIntoHttp() {
return true;
}
public void initSuccess(BaseResponse 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 {
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 extends BaseStateActivity implements AdapterCoverHelper {
@BindView(R.id.mRecyclerView)
public RecyclerView mRecyclerView;
@BindView(R.id.mNestedScrollView)
public NestedScrollView mNestedScrollView;
@BindView(R.id.refreshLayout)
public SmartRefreshLayout refreshLayout;
public List items = new ArrayList<>();
public BaseItemAdapter 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() {
@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 tClass = (Class)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
Type types = new TypeToken>() {
}.getType();
BaseResponse baseResponse = GsonUtils.fromJson(value.string(), types);
List 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> 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 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 extends BGARecyclerViewAdapter {
private AdapterCoverHelper coverHelper;
public BaseItemAdapter(RecyclerView recyclerView, int itemLayoutId, AdapterCoverHelper 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 {
void itemCover(BGAViewHolderHelper helper, int position, T model);
}
BaseItemActivity引用的xml
BaseStateActivity代码是我在BaseActivity基础上加了状态界面的封装代码,这里就不展示了,比较简单(状态:加载中,空界面,错误界面,无网络界面)
可以看到,我把RecyclerView,SmartRefreshLayout的默认配置,以及数据的获取,解析,和对网络获取数据成功失败的判断 操作都放在了BaseItemActivity中,大大减少了我们在编写列表是所需要的代码,本来数据解析是可以更简单一些的,可以由于泛型传达的擦除,导致需要重新解析一次,不过还是可以,毕竟以后书写的代码量少很香的,逻辑也相当的清晰,哈哈。
由于篇幅限制,第五种复杂列表封装在这里就先不记录了,毕竟用到的也比较少,一般主页大家也都不会用多列表来实现,而会选择碎片化的fragment,我也是因为项目需要自己写一个APP聊天系统,需要写聊天界面,所以才封装了这么一个复杂列表实现基类,下次有时间在记录吧。
老菜鸟水平不高,有大神浏览到还希望多给给意见,指出指出错误哈,不胜感激。