优化使用ListView、RecyclerView:应对低交互、特别多的不同类型布局的业务场景

出发点矛盾

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 
     * @return
     */
     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 mDatas;

    /**
     * 将数据重置
     *
     * @param datas
     */
    public  void setDatas(List datas) {
        ensureNotNull();
        mDatas.clear();
        if (hasContent(datas)) {
            mDatas.addAll(datas);
        }
        notifyDataChange(Change.SET, 0, mDatas.size() <= 0 ? 0 : mDatas.size() - 1);
    }

    /**
     * 将数据加入到列表末尾
     *
     * @param datas
     */
    public  void addDatas(List 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数据管理,对加入的List都去了具体类型引用,提供set add基本方法,适用于上拉加载,下拉刷新业务

管理bricks:

public class QUIBinderAttacher {

    private ArrayList 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 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 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 itemCache;

    public QUIRecyclerHolder(View itemView) {
        super(itemView);
        itemCache = new SparseArray<>(5);
    }

    @Override
    public View getItemView() {
        return itemView;
    }

    @Override
    public  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 {

    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()));
    }
}

整个工程如图:


优化使用ListView、RecyclerView:应对低交互、特别多的不同类型布局的业务场景_第1张图片
image.png

就是整个绑定逻辑

最终实践

模拟需求:
页面包含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 data = new ArrayList<>(3);
data.add(new Bean1("这是Bean1的内容"));
data.add(new Bean2(R.mipmap.ic_launcher_round, "这是Bean2的内容"));
adapter.getAdapterDatas().setDatas(data);
 
 

运行效果:


优化使用ListView、RecyclerView:应对低交互、特别多的不同类型布局的业务场景_第2张图片
image.png

如果这时候需求加入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种类型了,其他代码都不用修改,运行如下:


优化使用ListView、RecyclerView:应对低交互、特别多的不同类型布局的业务场景_第3张图片
image.png

如果到其他页面,我们使用的是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);

你可能感兴趣的:(优化使用ListView、RecyclerView:应对低交互、特别多的不同类型布局的业务场景)