美文网首页MobDevGroupandroid开发listview
优雅的实现多类型列表的Adapter

优雅的实现多类型列表的Adapter

作者: 红灰李 | 来源:发表于2017-01-04 16:41 被阅读5410次

引言

在开发中经常会遇到,一个列表(RecyclerView)中有多种布局类型的情况。前段时间,看到了这篇文章

[译]关于 Android Adapter,你的实现方式可能一直都有问题

文中主要从设计的角度阐释如何更合理的实现多种布局类型的Adapter,本文主要从实践的角度出发,站在巨人的肩膀上,结合我个人的理解进行阐述,如果有纰漏,欢迎留言指出。

有多种布局类型

有时候,由于应用场景的需要,列表(RecyclerView)中需要存在一种以上的布局类型。为了阐述的方便,我们先假设一种应用场景

列表中含有若干个常规的布局,在列表的中的第一个位置与第二个位置中分别为两个不同的布局,其余为常规的布局

针对这样的需求,笔者一直以来的实现方式如下

private final int ITEM_TYPE_ONE = 1;
private final int ITEM_TYPE_TWO = 2;

@Override
public int getItemViewType(int position) {
    if(0 == position){
       return  ITEM_TYPE_ONE;
    }else if(1 == position){
        return ITEM_TYPE_TWO;
    }
    return super.getItemViewType(position);
}
@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(ITEM_TYPE_ONE == viewType){
            return new OneViewHolder();
        }else if(ITEM_TYPE_TWO == viewType){
            return new TwoViewHolder();
        }
        return new NormalViewHolder();            
    }
@Override
//伪代码
    public void onBindViewHolder(ViewHolder holder, int position) {
       if(holder instanceof OneViewHolder ){
             ...
        }else if(holder instanceof TwoViewHolder){
             ...
        }else{
            ...
        }
    }
  • 在Adapter的getItemViewType方法中返回特定位置的特定标识(根据前文需求,就是position0与position1)
  • 在onCreateViewHolder中根据viewType参数,也就是getItemViewType的返回值来判断需要创建的ViewHolder类型
  • 在onBindViewHolder方法中对ViewHolder的具体类型进行判断,分别为不同类型的ViewHolder进行绑定数据与逻辑处理

通过以上就能实现多类型列表的Adapter,但这样的代码写多了总会觉得别扭,特别是看到了[译]关于 Android Adapter,你的实现方式可能一直都有问题这篇文章之后。

结合文章与我个人的理解,这种实现方式所存在弊端可以总结为以下几点:

  • 类型检查与类型转型,由于在onCreateViewHolder根据不同类型创建了不同的ViewHolder,所以在onBindViewHolder需要针对不同类型的ViewHolder进行数据绑定与逻辑处理,这导致需要通过instanceof对ViewHolder进行类型检查与类型转型。
    [译]关于 Android Adapter,你的实现方式可能一直都有问题中是这样说的

许多年前,我在我的显示器上贴了许多的名言。其中的一个来自 Scott Meyers 写的《Effective C++》 这本书(最好的IT书籍之一),它是这么说的:
不管什么时候,只要你发现自己写的代码类似于 “ if the object is of type T1, then do something, but if it’s of type T2, then do something else ”,就给自己一耳光

  • 不利于扩展,目前的需求是列表中存在三种布局类类型,那么如果需求变动,极端一点的情况就是数据源是从服务器获取的,数据中的model决定列表中的布局类型。这种情况下,每当model改变或model类型增加,我们都要去改变adapter中很多的代码,同时Adapter还必须知道特定的model在列表中的位置(position)除非跟服务端约定好,model(位置)不变,很显然,这是不现实的。
    [译]关于 Android Adapter,你的实现方式可能一直都有问题中是这样说的

另外,我们实行那些 adapter 的方法违背了 SOLID 原则中的“开闭准则” 。它是这样说的:“对扩展开放,对修改封闭。” 当我们添加另一个类型或者 model 到我们的类中时,比如叫 Rabbit 和 RabbitViewHolder,我们不得不在 Adapter 里改变许多的方法。 这是对开闭原则明显的违背。添加新对象不应该修改已存在的方法。

  • 不利于维护,这点应该是上一点的延伸,随着列表中布局类型的增加与变更,getItemViewType、onCreateViewHolder、onBindViewHolder中的代码都需要变更或增加,Adapter 中的代码会变得臃肿与混乱,增加了代码的维护成本。

首先让我摸摸自己的脸,然后结合[译]关于 Android Adapter,你的实现方式可能一直都有问题,看看如何优雅的实现多类型列表的Adapter

优雅的实现

结合上文,我们的核心目的就是三个

  • 避免类的类型检查与类型转型
  • 增强Adapter的扩展性
  • 增强Adapter的可维护性

前文提到了,当列表中类型增加或减少时Adapter中主要改动的就是getItemViewType、onCreateViewHolder、onBindViewHolder这三个方法,因此,我们就从这三个方法中开始着手。

Talk is cheap. Show me the code,围绕以上几点,开始码代码

getItemViewType

原本的代码是这样

@Override
public int getItemViewType(int position) {
    if(0 == position){
       return  ITEM_TYPE_ONE;
    }else if(1 == position){
        return ITEM_TYPE_TWO;
    }
    return super.getItemViewType(position);
}

在这段代码中,我们必须知道特定的布局类型在列表中的位置,而布局类型在列表中的位置是由数据源决定的,为了解决这个问题并且减少if之类的逻辑判断简化代码,我们可以简单粗暴的在Model中增加type标识,优化之后getItemViewType的实现大致如下

@Override
 public int getItemViewType(int position) {
    return modelList.get(position).getType();
 }

这样的方式有很大的局限性(谁用谁知道),这里就不展开了,直接看正确的姿势,先看代码(具体可以看源码

public interface Visitable {
    int type(TypeFactory typeFactory);
}

public class One implements Visitable {
    ...
    ...
    @Override
    public int type(TypeFactory typeFactory) {
        return typeFactory.type(this);
    }
}

public class Two implements Visitable {
    ...
    ...
    @Override
    public int type(TypeFactory typeFactory) {
        return typeFactory.type(this);
    }
}

public class Normal implements Visitable{
    ...
    ...
    @Override
    public int type(TypeFactory typeFactory) {
        return typeFactory.type(this);
    }
}

public interface TypeFactory {
    int type(One one);

    int type(Two two);
}

public class TypeFactoryForList implements TypeFactory {
    private final int TYPE_RESOURCE_ONE = R.layout.layout_item_one;
    private final int TYPE_RESOURCE_TWO = R.layout.layout_item_two;
    private final int TYPE_RESOURCE_NORMAL = R.layout.layout_item_normal;
    @Override
    public int type(One one) {
        return TYPE_RESOURCE_ONE;
    }

    @Override
    public int type(Two one) {
        return TYPE_RESOURCE_TWO;
    }

    @Override
    public int type(Normal normal) {
        return TYPE_RESOURCE_NORMAL;
    }
    ...
}

针对getItemViewType可以进行如下实现

private List<Visitable> modelList;
@Override
public int getItemViewType(int position) {
    return modelList.get(position).type(typeFactory);
 }

小结

  • 通过接口抽象,将所有与列表相关的Model抽象为Visitable,当我们在初始化数据源时就能以List<Visitable>的形式将不同类型的Model集合在列表中;
  • 通过访问者模式,将列表类型判断的相关代码抽取到TypeFactoryForList 中,同时所有列表类型对应的布局资源都在这个类中进行管理与维护,以这样的方式巧妙的增强了扩展性与可维护性;
  • getItemViewType中不再需要进行if判断,通过数据源控制列表的布局类型,同时返回的不再是简单的布局类型标识,而是布局的资源ID(通过modelList.get(position).type()获取),进一步简化代码(在onCreateViewHolder中会体现出来);

onCreateViewHolder

结合上文可以了解到,getItemViewType返回的是布局资源ID,也就是onCreateViewHolder(ViewGroup parent, int viewType)参数中的viewType,我们可以直接用viewType创建itemView,但是,问题来了,itemView创建之后,还是需要进行类型判断,创建不同的ViewHolder,针对这个问题可以分以下几个步骤解决
首先为了增强ViewHolder的灵活性,可以继承RecyclerView.ViewHolder派生出BaseViewHolder抽象类如下

public abstract class BaseViewHolder<T> extends RecyclerView.ViewHolder {
    private SparseArray<View> views;
    private View mItemView;
    public BaseViewHolder(View itemView) {
        super(itemView);
        views = new SparseArray<>();
        this.mItemView = itemView;
    }

    public View getView(int resID) {
        View view = views.get(resID);

        if (view == null) {
            view = mItemView.findViewById(resID);
            views.put(resID,view);
        }

        return view;
    }

    public abstract void setUpView(T model, int position, MultiTypeAdapter adapter);
}

不同的ViewHolder继承BaseViewHolder并实现setUpView方法即可。

然后对TypeFactory 与TypeFactoryForList 增加如下代码

public interface TypeFactory {
  ...
  BaseViewHolder createViewHolder(int type, View itemView);
}

public class TypeFactoryForList implements TypeFactory {
  private final int TYPE_RESOURCE_ONE = R.layout.layout_item_one;
  private final int TYPE_RESOURCE_TWO = R.layout.layout_item_two;
  private final int TYPE_RESOURCE_NORMAL = R.layout.layout_item_normal;
  ...
  @Override
  public BaseViewHolder createViewHolder(int type, View itemView) {

        if(TYPE_RESOURCE_ONE == type){
            return new OneViewHolder(itemView);
        }else if (TYPE_RESOURCE_TWO == type){
            return new TwoViewHolder(itemView);
        }else if (TYPE_RESOURCE_NORMAL == type){
            return new NormalViewHolder(itemView);
        }

        return null;
    }
}

最后对onCreateViewHolder方法进行如下实现

@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    Context context = parent.getContext();

    View itemView = View.inflate(context,viewType,null);
    return typeFactory.createViewHolder(viewType,itemView);
}

小结

  • 在onCreateViewHolder中以BaseViewHolder作为返回值类型。因为BaseViewHolder作为不同类型的ViewHolder的基类,可以避免在onBindViewHolder中对ViewHolder进行类型检查与类型转换,同时也可以简化onBindViewHolder方法中的代码(具体会在下文阐述);
  • 创建不同类型的ViewHolder的相关代码被抽取到了TypeFactoryForList 中,简化了onCreateViewHolder中的代码,同时与类型相关的代码都集中在TypeFactoryForList 中,方便后期维护与拓展;

onBindViewHolder

经过以上实现,onBindViewHolder中的代码就非常的轻盈了,如下

@Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
    holder.setUpView(models.get(position),position,this);
}

可以看到,在onBindViewHolder中不需要对ViewHolder进行类型检查与转换,也不需要针对不同类型的ViewHoler执行不同绑定操作,不同的列表布局类型的数据绑定(逻辑代码)都交给了与其自身对应的ViewHolder处理,如下(setUpView中的代码可根据实际情况修改)

public class NormalViewHolder extends BaseViewHolder<Normal> {
    public NormalViewHolder(View itemView) {
        super(itemView);
    }

    @Override
    public void setUpView(final Normal model, int position, MultiTypeAdapter adapter) {
        final TextView textView = (TextView) getView(R.id.normal_title);
        textView.setText(model.getText());

        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(textView.getContext(),model.getText(),Toast.LENGTH_SHORT).show();
            }
        });
    }
}

小结

  • onBindViewHolder中不需要进行类型检查与转换,对ItemView的数据绑定与逻辑处理都交由各自的ViewHolder进行处理。通过这样方式,让代码更整洁,更易于维护,同时也增强了扩展性。

总结

经过如上优化之后,Adapter中的代码如下

public class MultiTypeAdapter extends RecyclerView.Adapter<BaseViewHolder> {
    private TypeFactory typeFactory;
    private List<Visitable> models;

    public MultiTypeAdapter(List<Visitable> models) {
        this.models = models;
        this.typeFactory = new TypeFactoryForList();

    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Context context = parent.getContext();

        View itemView = View.inflate(context,viewType,null);
        return typeFactory.createViewHolder(viewType,itemView);
    }

    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        holder.setUpView(models.get(position),position,this);
    }

    @Override
    public int getItemCount() {
        if(null == models){
            return 0; 
        }
        return models.size();
    }


    @Override
    public int getItemViewType(int position) {
        return models.get(position).type(typeFactory);
    }
    
}

当列表中增加类型时:

  • 为该类型创建实现了Visitable接口的Model类
  • 创建继承于BaseViewHolder的ViewHolder(与Model类对应)
  • 为TypeFactory增加type方法(与Model类对应) ,同时TypeFactoryForList 实现该方法
  • 为TypeFactoryForList增加与列表类型对应的资源ID参数
  • 修改TypeFactoryForList 中的createViewHolder方法

可以看到,虽然Adapter中的代码量减少,但总体的代码量并没减少(可能还增多了),但是和好处比起来,增加一点代码量还是值得的

  • 拓展性——Adapter并不关心不同的列表类型在列表中的位置,因此对于Adapter来说列表类型可以随意增加或减少,我们只需要维护好数据源即可。
  • 可维护性——不同的列表类型由不同的ViewHolder维护,相互之间互不干扰;对类型的管理都在TypeFactoryForList 中,TypeFactoryForList 中的代码量少,代码简洁,维护成本低。
  • 避免了类的类型检查与类型转型,这点看源码就可以知道

源码地址

最后

可能还有待完善的地方,大家可以根据实际情况进行修改与扩展。同时,欢迎留言交流。

参考:
[译]关于 Android Adapter,你的实现方式可能一直都有问题

相关文章

网友评论

  • AylmerChen:感觉还是违背了开闭原则,访问者模式用于数据结构稳定,而操作变化的场合,但这里是操作只有 typeFactory, 而数据结构却是变化的,每次还是要改不少,但是 adapter 的确是不用改了,真是两权相害取其清……
  • Cpb:谢谢楼主,虽然还是没能完全消化,但能体会到那种优秀的设计思想了。:+1:
  • 一只大BUG:加入某个item就需要一个简单string,就蹦实现Visitable接口了啊,请问这时候有什么好的解决办法没有
    一只大BUG:总不能为每种基本类型 常见类型都写一个wrapper吧
  • 甜牛奶苦咖啡:如果type是从服务端传过来的该如何改造现在的代码
  • 8ced2ce24185:你好 我对你的代码进行部分修改 添加type比较方便,更容易使用。请指教
    https://github.com/lizhifeng-sky/MultipleAdapter/tree/master
    一只大BUG:我的解决方案 https://github.com/RunFeifei/Recapter/tree/master/recater/src/main/java/com/fei/root/recater/adapter/multi 不需要重写Adapter和viewholder 并支持基本类型.
    8ced2ce24185:@落雨收柴 谢谢 这点没注意到 :smile:
    落雨收柴:楼主实现是用访问者模式动态增加model的功能,把返回ViewItemType的功能从model里面提取了出来,放到了访问者对象里面;你是去掉了访问者模式,直接为每一个model添加一个可以返回ViewItemType的父类。看这篇文章http://www.jianshu.com/p/c6a44e18badb,他这么用是为了防止在model类里面写presenter层的代码。
  • kotlon:学习了,思路确实不错,最近的开发也碰到这个问题,当时考虑思路不太一样,我当时是想通过一个HashMap去实现mutilType,但是难以避免的在 onBindBiewHolder()里面碰到了转型问题,
  • 89c5e6addf81:感觉每次还是会改很多,遵守开闭准则实现了一个,望楼主评价一下
    https://github.com/jarlen/RichCommon
  • 珞泽珈群:细度文章很有收获,但是发现几个问题。
    决定关系position->viewType->viewHolder,所以在onBindViewHolder(ViewHolder holder, int position)中holder的类型是确定的,并不需要
    if(holder instanceof OneViewHolder ){
    ...
    }else if(holder instanceof TwoViewHolder){
    ...
    直接强制类型转换就可以了(OneViewHolder)holder。

    用TypeFactoryForList 统一管理各种类型是个很好的设计,但是目前这个设计对于扩展并不十分开放。如果要添加新的类型,必须修改TypeFactory接口,增加一个方法int type(...);并且在TypeFactoryForList增加一个private final int TYPE的字段,并实现新增的int type()方法。如果TypeFactoryForList中有个addType的方法,那么就可以在不修改TypeFactoryForList的前提下直接增加一种类型,但是楼主用的是重载函数int type(),让type()根据不同的类型自动调用不同的方法,这虽然不需要instanceof的类型检查,但是也破坏了一点扩展性。

  • 73ece15c815f:布局控件的点击事件没有问题,但是做整个item的点击事就不起作用啊 。 为什么呢 我给item布局的最外层加了个id 然后写的点击事件
    73ece15c815f:@小呢个李 我去给item 添加一下消费事件的先后顺序 试一下
    73ece15c815f:@红灰李 就是用的你的demo 是textview 不应该啊
    红灰李:@小呢个李 看一下是不是布局内部的子控件把事件消费掉了
  • 落雨收柴:楼主看一下这种动态设置列数有没有问题:
    1.接口Visitable添加方法,每一个实体类去实现,返回它所占的比例:
    int getSpanSize();
    2.MutliAdapter中添加方法,用来返回每一个实体所占的比例:

    public int getSpanSize(int position){
    return models.get(position).getSpanSize();
    }
    3.MainActivity中:
    GridLayoutManager layout = new GridLayoutManager(this, 4);
    layout.setOrientation(LinearLayoutManager.VERTICAL);
    layout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
    return mMultiTypeAdapter.getSpanSize(position);
    }
    });
    recyclerView.setLayoutManager(layout);
    4.我使用这种方法实现的列数控制,但是不知道好不好,求解......
    红灰李:@落雨收柴 其实只是把各个Model类中相同的逻辑集中起来 代码量应该不会多 回头我去实践一下
    落雨收柴:@红灰李 的确没有用访问者模式,但是这里再用的话,无用代码就加了好多,尤其实体类增加的时候,其实我不太懂访问者模式....
    红灰李: 好不好这个因实际需求而定了,如果我来实现的话,我也会采用你这样的实现方式。
    不知道第一步中getSpanSize()是不是也是通过访问者模式来实现的呢,把类型对应的列数通过TypeFactoryForList返回,这样比较符合Visitable这个接口的定义,同时也比较好管理,和类型相关的代码都在TypeFactoryForList中。
  • North_2016:思路不错,整个TypeFactory还可以由apt自动生成
  • 96abba5ac0ae:我想了解一下,这个是自己new的model,但是如果后台那边返回的数据,怎么解析成对应的model呀!
    红灰李:@96abba5ac0ae 分别为这三个类型创建三个实现了Visitable接口的类就可以了
    96abba5ac0ae:@红灰李 [{"resId":"0","text":"我是普通的类型"},{"resId":"0","text":"我是普通的类型"},{"resId":"0","text":"我是普通的类型"},{"text":"我是类型一"},{"text":"我是类型一"},{"text":"我是类型一"},{"text":"我是类型一"},{"resId":"0"},{"resId":"0"},{"resId":"0"},{"resId":"0"},{"resId":"0"}]我现在数据是这样的,我想解析成三个model,去做布局的适配,怎么去做?
    红灰李:@96abba5ac0ae 这个可以根据服务器给的api文档设计好客户端的数据模型即可
  • 3cf3cd4169d3:将bindView这部分代码转到viewHolder中,这一步非常漂亮,逻辑将变得非常清晰
  • Allen_99:如果可以动态设置列数会更好了,
  • 006b49b50439:楼主你好 我下载了你的源码 运行 发现只支持 LinearLayoutManager 布局 如果是GridLayoutManager 的话 One Two Three 三个item都会成grid布局了 有没有方法可以设置 gridlayout 布局之作用在normalitem里面 其它都是LinearLayout
    006b49b50439:@红灰李 刚发完我就想到这个方法了 可行的 所以 我白问了
    还有 如果能添加上拉加载状态 的footer 就完美了
    红灰李:@rgg 你说的这种情况我还真没有考虑到 :sweat_smile: 根据你的描述 我不知道你有没有试过GridLayoutManager的setSpanSizeLookup方法去动态改变列数
  • 扣子兮兮:getView 写的是不是有问题啊,你添加布局和previews显示的是不一样。
    红灰李:@96abba5ac0ae 382039099
    96abba5ac0ae:@红灰李 能不能留个QQ,想请教一些东西
    红灰李:@fix扣子 可以详细描述一下吗
  • smartapple:那如果增加header 或者 footer呢
    54359c44b035:@双开门 header和footer如果按楼主的做法,要在models里面加伪数据才能做吧
    但是这样就修改了原始数据了。除非是ViewModel 和 dataModel的结构,再增加一个headType和footType
    dc36981ec4e1:header 或者footer 不也是recycle 的一个item么 当作item处理不就好了
    红灰李:@四条眼的小胖子 要看具体情况了 如果只是固定在头部和尾部那没什么区别
  • 5c5aabd47927:收货颇丰,感谢楼主

本文标题:优雅的实现多类型列表的Adapter

本文链接:https://www.haomeiwen.com/subject/acdnvttx.html