优化使用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 <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()));
    }
}

整个工程如图:


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<Object> data = new ArrayList<>(3);
data.add(new Bean1("这是Bean1的内容"));
data.add(new Bean2(R.mipmap.ic_launcher_round, "这是Bean2的内容"));
adapter.getAdapterDatas().setDatas(data);

运行效果:


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种类型了,其他代码都不用修改,运行如下:


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);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容