出发点矛盾
1.业务稍微复杂,变动大
近产品大开脑洞,APP首页列表,身兼多种业务,每种业务,又分数据视图和没数据的占位视图,整体就跟淘宝首页类似,还要插入不同类型的广告业务视图(差不多广告有2-4种不同UI类型),加上需求变来变去,UI变化很大,搞的我很头疼。
2.偷懒,不想多次处理老代码
以RecyclerView的Adapter举例,如果我们要多类型,每次要定义type=0,1,2....,再到CreateHolder,处理type=0,1,2....如果有新增的类型,又要修改这部分逻辑,只想偷懒,每次新业务,都是即插即用,不再考虑下面这些重复判定逻辑:
@Override
public int getItemViewType(int position) {
// TODO Auto-generated method stub
Object obj = mDataList.get(position);
if (obj instanceof XXXX) {
return 0;
} else if (obj instanceof YYYYY) {
return 1;
}
return super.getItemViewType(position);
}
@Override
public Object createViewHolder(int type, ....) {
if (TYPE_ONE == type) {
return XXXViewHolder;
} else if (TYPE_TWO == type) {
return YYYYViewHolder;
}
return null;
}
3.尽可能复用
再者老代码是listview,新代码部分使用的是recycleView,面对两者不同的adapter适配,我想同一套业务逻辑统一逻辑调用,而不要出现针对不同视图列表,定义新的业务逻辑绑定
解决目标、场景
1.每个业务视图,逻辑独立,可替换,可删除,有效适应业务变换,新增业务,不影响老业务
2.针对Listview、RecycleView,不同列表视图无差别化
3.目前我们APP的业务视图,是弱交互场景,基本都是点击后跳独立页面
思路方向
每个业务,有个我们可以确定的事情:
1.针对一个adapter,它的每个数据,就知道自己是怎样的业务视图
2.每个业务item,要做的事情很明确,拿到数据,初始化布局,将业务数据绑定
所以,抽象2个地方:
1.业务绑定逻辑,定义为 XXXXBinder,称之为 Bricks(积木)
2.业务UI视图,定义为XXXXHolder,视图集合体
3.adapter 内部统一数据管理
4.adapter 内部统一Bricks管理
通过数据去找Bricks,来绑定业务逻辑
1.抽象出原始Bricks,视图逻辑
提高抽象逻辑,对bricks屏蔽具体底层是listView类型的adapter、还是RecycleView的adapter
我不希望,同样的业务bricks,在不同的列表视图还需要分别定义
抽象出通用视图逻辑:
public interface QUIAdapterHolder {
/**
* 获取到ItemView
*
* @return
*/
View getItemView();
/**
* 获取ItemView 下的具体子view
*
* @param id
* @param <T>
* @return
*/
<T extends View> T findView(int id);
}
item视图,最基本只需要提供查找view的功能即可。
抽象出业务逻辑(Bricks):
public interface QUIAdapterBinder {
/**
* 因为我们是通过数据反查业务逻辑,所以每个业务定义,需要告诉外部adapter自己接受什么样的数据
*
* @param position
* @param data
* @return false不处理,true将触发bindDataToView
*/
boolean accept(int position, Object data);
/**
* 获取当前业务对应的布局
*
* @return
*/
int getBinderContentView();
/**
* 绑定当前业务数据
*
* @param context
* @param holder 视图逻辑
* @param position
* @param data
*/
void bindDataToView(Context context, QUIAdapterHolder holder, int position, Object data);
}
这里做了一个处理,所有数据对象统一处理为Object引用,使得adapter内部逻辑简单,数据只能进入对应业务accept()、bindDataToView()方法的时候,再强转为业务类进行处理
2.实现Adapter内部逻辑
管理数据逻辑:
public abstract class QUIAdapterData {
public enum Change {
SET,
ADD
}
private List<Object> mDatas;
/**
* 将数据重置
*
* @param datas
*/
public <T> void setDatas(List<T> datas) {
ensureNotNull();
mDatas.clear();
if (hasContent(datas)) {
mDatas.addAll(datas);
}
notifyDataChange(Change.SET, 0, mDatas.size() <= 0 ? 0 : mDatas.size() - 1);
}
/**
* 将数据加入到列表末尾
*
* @param datas
*/
public <T> void addDatas(List<T> datas) {
ensureNotNull();
int start = mDatas.size() > 0 ? mDatas.size() - 1 : 0;
if (hasContent(datas)) {
mDatas.addAll(datas);
notifyDataChange(Change.ADD, start, mDatas.size() - 1);
}
}
/**
* 获取数据长度
*
* @return
*/
public int getDataCount() {
return mDatas == null ? 0 : mDatas.size();
}
/**
* 获取到对应的数据
*
* @param position
* @return
*/
public Object getData(int position) {
if (position < 0 || position >= mDatas.size()) {
throw new IllegalStateException(String.format("非法postion[%d], dataSize[%d]", position, mDatas.size()));
}
return mDatas.get(position);
}
/**
* 数据发生了变动
*
* @param change
* @param startPosition
* @param endPosition
*/
abstract void notifyDataChange(QUIAdapterData.Change change, int startPosition, int endPosition);
private void ensureNotNull() {
if (mDatas == null) {
mDatas = new ArrayList<>(20);
}
}
private boolean hasContent(List<?> datas) {
return datas != null && !datas.isEmpty();
}
}
可以看出,QUIAdapterData 提供了List<Object>数据管理,对加入的List<T>都去了具体类型引用,提供set add基本方法,适用于上拉加载,下拉刷新业务
管理bricks:
public class QUIBinderAttacher {
private ArrayList<QUIAdapterBinder> mBinderCache = new ArrayList<>(5);
public QUIBinderAttacher addBinder(QUIAdapterBinder binder) {
mBinderCache.add(binder);
return this;
}
/**
* 根据adapter传递的 position, 数据
*
* @param outPosition
* @param data
* @return
*/
public QUIAdapterBinder findBinder(int outPosition, Object data) {
int index = getBinderType(outPosition, data);
if (index < 0) {
return QUINotFoundBinder.get();
}
return mBinderCache.get(index);
}
/**
* 根据外部的type返回对应的binder
*
* @param outType
* @return
*/
public QUIAdapterBinder findBinder(int outType) {
try {
return mBinderCache.get(outType);
} catch (Exception e) {
return QUINotFoundBinder.get();
}
}
/**
* 获取binder种类数量
*
* @return
*/
public int getBinderCount() {
return mBinderCache.size() + 1;//实际处理类型 + UncatchType
}
/**
* 根据position, data,匹配对应的binder
* TODO://这个函数可能有性能问题, 数据是 count * type count 次循环
*
* @param data
* @return 如果没有查询到 返回-1 ,如果查询到了将index作为binder的type返回给adapter
*/
public int getBinderType(int outPosition, Object data) {
if (mBinderCache != null) {
for (int i = 0, size = mBinderCache.size(); i < size; i++) {
if (mBinderCache.get(i).accept(outPosition, data)) {
return i;
}
}
}
return -1;
}
}
这里QUINotFoundBinder 是一个默认实现,如果找不到对应处理某个Object的Bricks,则以TextView的形式展示错误信息
通过QUIBinderAttacher .mBinderCache的index,来标记不同业务的type
通过QUIBinderAttacher .getBinderType里面,调用Bricks的accept方法,来达成数据反查业务逻辑的思路
3.搭建ListView相关Adapter, VHolder
首先实现QUIListHolder:
public class QUIListHolder implements QUIAdapterHolder {
private View itemView;
private SparseArray<View> itemCache;
public QUIListHolder(View itemView) {
this.itemView = itemView;
this.itemCache = new SparseArray<>(5);
itemView.setTag(this);
}
@Override
public View getItemView() {
return itemView;
}
@Override
public <T extends View> T findView(int id) {
View view = null;
if ((view = itemCache.get(id)) != null) {
return (T) view;
}
view = itemView.findViewById(id);
itemCache.put(id, view);
return (T) view;
}
}
没有什么特别的,就是沿用以前ListView的ViewHolder处理
再次,实现QUIListAdapter:
/**
* cn.quick.ui.widget.adapter
* 通用listView, gridView adapter
* TODO 设置给listview adapter的时候,一定要在attach所有binder 之后
*
* @author qiuh
* @date 2017/11/17 0017 14:06
*/
public class QUIListAdapter extends BaseAdapter {
private Context mCtx;
private QUIAdapterData mDatas;
private QUIBinderAttacher mAttacher;
public QUIListAdapter(Context context) {
mDatas = new QUIAdapterData() {
@Override
void notifyDataChange(Change change, int startPosition, int endPosition) {
notifyDataSetChanged();
}
};
mCtx = context;
mAttacher = new QUIBinderAttacher();
}
/**
* 拿到内部数据
*
* @return
*/
public QUIAdapterData getAdapterDatas() {
return mDatas;
}
/**
* 获取UI binder加载器
*
* @return
*/
public QUIBinderAttacher getAdapterAttacher() {
return mAttacher;
}
@Override
public int getItemViewType(int position) {
return mAttacher.getBinderType(position, mDatas.getData(position));
}
@Override
public int getViewTypeCount() {
return mAttacher.getBinderCount();
}
@Override
public int getCount() {
return mDatas.getDataCount();
}
@Override
public Object getItem(int position) {
return mDatas.getData(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Object data = getItem(position);
QUIAdapterBinder binder = mAttacher.findBinder(position, data);
if (convertView == null) {
convertView = LayoutInflater.from(mCtx).inflate(binder.getBinderContentView(), parent, false);
}
QUIListHolder vholder = (QUIListHolder) convertView.getTag();
if (vholder == null) {
vholder = new QUIListHolder(convertView);
}
binder.bindDataToView(mCtx, vholder, position, data);
return convertView;
}
}
由于数据管理,Bricks管理,我们已经单独抽取了,所以整个Adapter就简单明了,包括注释不超过100行
注意:一定要在Adapter.attach所有Bricks之后,再调用列表视图的setAdapter(),设置适配器
4.搭建RecycleView相关Adapter, VHolder
按部就班,编写QUIRecyclerHolder:
public class QUIRecyclerHolder extends RecyclerView.ViewHolder implements QUIAdapterHolder {
private SparseArray<View> itemCache;
public QUIRecyclerHolder(View itemView) {
super(itemView);
itemCache = new SparseArray<>(5);
}
@Override
public View getItemView() {
return itemView;
}
@Override
public <T extends View> T findView(int id) {
View view = null;
if ((view = itemCache.get(id)) != null) {
return (T) view;
}
view = itemView.findViewById(id);
itemCache.put(id, view);
return (T) view;
}
}
然后是QUIRecyclerAdapter的实现
public class QUIRecyclerAdapter extends RecyclerView.Adapter<QUIRecyclerHolder> {
private Context mCtx;
private QUIAdapterData mDatas;
private QUIBinderAttacher mAttacher;
public QUIRecyclerAdapter(Context context) {
this.mCtx = context;
this.mDatas = new QUIAdapterData() {
@Override
void notifyDataChange(Change change, int startPosition, int endPosition) {
if (change == Change.SET) {
notifyDataSetChanged();
} else {
notifyItemRangeInserted(startPosition, endPosition);
}
}
};
this.mAttacher = new QUIBinderAttacher();
}
public QUIRecyclerAdapter(Context context, QUIAdapterData data) {
this.mCtx = context;
this.mDatas = data;
this.mAttacher = new QUIBinderAttacher();
}
/**
* 拿到内部数据
*
* @return
*/
public QUIAdapterData getAdapterDatas() {
return mDatas;
}
/**
* 获取UI binder加载器
*
* @return
*/
public QUIBinderAttacher getAdapterAttacher() {
return mAttacher;
}
@Override
public int getItemViewType(int position) {
return mAttacher.getBinderType(position, mDatas.getData(position));
}
@Override
public QUIRecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new QUIRecyclerHolder(LayoutInflater.from(mCtx)
.inflate(mAttacher.findBinder(viewType).getBinderContentView(), parent, false));
}
@Override
public void onBindViewHolder(QUIRecyclerHolder holder, int position) {
QUIAdapterBinder binder = mAttacher.findBinder(getItemViewType(position));
binder.bindDataToView(mCtx, holder, position, mDatas.getData(position));
}
@Override
public int getItemCount() {
return mDatas.getDataCount();
}
}
同ListView的adapter一样,逻辑简单,通过QUIBinderAttacher 去获取不同类型的Bricks,这样同样的Bricks在ListView RecyclerView,用法都一样。
默认找不到处理类型的实现:
class QUINotFoundBinder implements QUIAdapterBinder {
public static QUINotFoundBinder get() {
return Inner.M;
}
private static class Inner {
public static QUINotFoundBinder M = new QUINotFoundBinder();
}
private QUINotFoundBinder() {
}
@Override
public boolean accept(int position, Object data) {
return true;
}
@Override
public int getBinderContentView() {
return android.R.layout.simple_list_item_1;
}
@Override
public void bindDataToView(Context context, QUIAdapterHolder holder, int position, Object data) {
TextView view = (TextView) holder.getItemView();
view.setText(String.format("找不到对应类型的解析器: value=[%s] clz=[%s]", data.toString(), data.getClass().getSimpleName()));
}
}
整个工程如图:
就是整个绑定逻辑
最终实践
模拟需求:
页面包含2个业务模型,Bean1 是单纯显示一段文字,Bean2 是大图+文字
我们不再关心Adapter的逻辑了,我只需要专心写2个Bricks,文本针对Bean1, Bean2所代表的业务场景:
private class Bean1Binder implements QUIAdapterBinder {
@Override
public boolean accept(int position, Object data) {
return data.getClass() == Bean1.class;
}
@Override
public int getBinderContentView() {
return android.R.layout.simple_list_item_1;
}
@Override
public void bindDataToView(Context context, QUIAdapterHolder holder, int position, Object data) {
Bean1 content = (Bean1) data;
TextView view = holder.findView(android.R.id.text1);
view.setText(content.content);
}
}
private class Bean2Binder implements QUIAdapterBinder {
@Override
public boolean accept(int position, Object data) {
return data.getClass() == Bean2.class;
}
@Override
public int getBinderContentView() {
return R.layout.item_photo_text;
}
@Override
public void bindDataToView(Context context, QUIAdapterHolder holder, int position, Object data) {
Bean2 content = (Bean2) data;
TextView vtext = holder.findView(R.id.photo_text_text);
ImageView vicon = holder.findView(R.id.photo_text_photo);
vicon.setImageResource(content.iconRes);
vtext.setText(content.content);
}
}
private class Bean1 {
String content;
public Bean1(String content) {
this.content = content;
}
}
private class Bean2 {
String content;
int iconRes;
public Bean2(int iconRes, String content) {
this.content = content;
this.iconRes = iconRes;
}
}
这样2个业务场景绑定就完成了,如何使用:
ListView vlist = findViewById(R.id.test_list);
QUIListAdapter adapter = new QUIListAdapter(this);
adapter.getAdapterAttacher()
.addBinder(new Bean1Binder())
.addBinder(new Bean2Binder());
vlist.setAdapter(adapter);
//数据统一加入Object 列表里面
List<Object> data = new ArrayList<>(3);
data.add(new Bean1("这是Bean1的内容"));
data.add(new Bean2(R.mipmap.ic_launcher_round, "这是Bean2的内容"));
adapter.getAdapterDatas().setDatas(data);
运行效果:
如果这时候需求加入Bean3的场景,以前的代码,不用动,单独编写Bean3的Bricks:
private class Bean3Binder implements QUIAdapterBinder {
@Override
public boolean accept(int position, Object data) {
return data.getClass() == Bean3.class;
}
@Override
public int getBinderContentView() {
return R.layout.item_photo_text2;
}
@Override
public void bindDataToView(Context context, QUIAdapterHolder holder, int position, Object data) {
Bean3 content = (Bean3) data;
TextView vtext = holder.findView(R.id.photo_text_text);
ImageView vicon = holder.findView(R.id.photo_text_photo);
ImageView vicon2 = holder.findView(R.id.photo_text_photo2);
vicon.setImageResource(content.iconRes);
vicon2.setImageResource(content.iconRes2);
vtext.setText(content.content);
}
}
private class Bean3 {
String content;
int iconRes;
int iconRes2;
public Bean3(int iconRes, int iconRes2, String content) {
this.content = content;
this.iconRes = iconRes;
this.iconRes2 = iconRes2;
}
}
然后对Adapter新增Bricks
adapter.getAdapterAttacher()
.addBinder(new Bean1Binder())
.addBinder(new Bean2Binder())
.addBinder(new Bean3Binder());
这样我们的列表就支持3种类型了,其他代码都不用修改,运行如下:
如果到其他页面,我们使用的是RecycleView怎么办?很简单,我们只要修改一下Adapter,其他都不用变:
RecyclerView vlist = findViewById(R.id.test_list);
QUIRecyclerAdapter adapter = new QUIRecyclerAdapter(this);
adapter.getAdapterAttacher()
.addBinder(new Bean1Binder())
.addBinder(new Bean2Binder())
.addBinder(new Bean3Binder());
vlist.setAdapter(adapter);