Android开发-优雅的实现多类型列表的Adapter

编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!  

 
pexels-photo-50631.jpeg

引言

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

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

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

有多种布局类型

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

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

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

 
 
  1. private final int ITEM_TYPE_ONE = 1;
  2. private final int ITEM_TYPE_TWO = 2;
  3.  
  4. @Override
  5. public int getItemViewType(int position) {
  6.     if(0 == position){
  7.        return  ITEM_TYPE_ONE;
  8.     }else if(1 == position){
  9.         return ITEM_TYPE_TWO;
  10.     }
  11.     return super.getItemViewType(position);
  12. }
 
 
  1. @Override
  2.     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  3.         if(ITEM_TYPE_ONE == viewType){
  4.             return new OneViewHolder();
  5.         }else if(ITEM_TYPE_TWO == viewType){
  6.             return new TwoViewHolder();
  7.         }
  8.         return new NormalViewHolder();            
  9.     }
 
 
  1. @Override
  2. //伪代码
  3.     public void onBindViewHolder(ViewHolder holder, int position) {
  4.        if(holder instanceof OneViewHolder ){
  5.              ...
  6.         }else if(holder instanceof TwoViewHolder){
  7.              ...
  8.         }else{
  9.             ...
  10.         }
  11.     }
  • 在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

原本的代码是这样

 
 
  1. @Override
  2. public int getItemViewType(int position) {
  3.     if(0 == position){
  4.        return  ITEM_TYPE_ONE;
  5.     }else if(1 == position){
  6.         return ITEM_TYPE_TWO;
  7.     }
  8.     return super.getItemViewType(position);
  9. }

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

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

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

 
 
  1. public interface Visitable {
  2.     int type(TypeFactory typeFactory);
  3. }
  4.  
  5. public class One implements Visitable {
  6.     ...
  7.     ...
  8.     @Override
  9.     public int type(TypeFactory typeFactory) {
  10.         return typeFactory.type(this);
  11.     }
  12. }
  13.  
  14. public class Two implements Visitable {
  15.     ...
  16.     ...
  17.     @Override
  18.     public int type(TypeFactory typeFactory) {
  19.         return typeFactory.type(this);
  20.     }
  21. }
  22.  
  23. public class Normal implements Visitable{
  24.     ...
  25.     ...
  26.     @Override
  27.     public int type(TypeFactory typeFactory) {
  28.         return typeFactory.type(this);
  29.     }
  30. }
  31.  
  32. public interface TypeFactory {
  33.     int type(One one);
  34.  
  35.     int type(Two two);
  36. }
  37.  
  38. public class TypeFactoryForList implements TypeFactory {
  39.     private final int TYPE_RESOURCE_ONE = R.layout.layout_item_one;
  40.     private final int TYPE_RESOURCE_TWO = R.layout.layout_item_two;
  41.     private final int TYPE_RESOURCE_NORMAL = R.layout.layout_item_normal;
  42.     @Override
  43.     public int type(One one) {
  44.         return TYPE_RESOURCE_ONE;
  45.     }
  46.  
  47.     @Override
  48.     public int type(Two one) {
  49.         return TYPE_RESOURCE_TWO;
  50.     }
  51.  
  52.     @Override
  53.     public int type(Normal normal) {
  54.         return TYPE_RESOURCE_NORMAL;
  55.     }
  56.     ...
  57. }

针对getItemViewType可以进行如下实现

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

小结

  • 通过接口抽象,将所有与列表相关的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抽象类如下

 
 
  1. public abstract class BaseViewHolder<T> extends RecyclerView.ViewHolder {
  2.     private SparseArray<View> views;
  3.     private View mItemView;
  4.     public BaseViewHolder(View itemView) {
  5.         super(itemView);
  6.         views = new SparseArray<>();
  7.         this.mItemView = itemView;
  8.     }
  9.  
  10.     public View getView(int resID) {
  11.         View view = views.get(resID);
  12.  
  13.         if (view == null) {
  14.             view = mItemView.findViewById(resID);
  15.             views.put(resID,view);
  16.         }
  17.  
  18.         return view;
  19.     }
  20.  
  21.     public abstract void setUpView(T model, int position, MultiTypeAdapter adapter);
  22. }

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

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

 
 
  1. public interface TypeFactory {
  2.   ...
  3.   BaseViewHolder createViewHolder(int type, View itemView);
  4. }
  5.  
  6. public class TypeFactoryForList implements TypeFactory {
  7.   private final int TYPE_RESOURCE_ONE = R.layout.layout_item_one;
  8.   private final int TYPE_RESOURCE_TWO = R.layout.layout_item_two;
  9.   private final int TYPE_RESOURCE_NORMAL = R.layout.layout_item_normal;
  10.   ...
  11.   @Override
  12.   public BaseViewHolder createViewHolder(int type, View itemView) {
  13.  
  14.         if(TYPE_RESOURCE_ONE == type){
  15.             return new OneViewHolder(itemView);
  16.         }else if (TYPE_RESOURCE_TWO == type){
  17.             return new TwoViewHolder(itemView);
  18.         }else if (TYPE_RESOURCE_NORMAL == type){
  19.             return new NormalViewHolder(itemView);
  20.         }
  21.  
  22.         return null;
  23.     }
  24. }

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

 
 
  1. @Override
  2. public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  3.     Context context = parent.getContext();
  4.  
  5.     View itemView = View.inflate(context,viewType,null);
  6.     return typeFactory.createViewHolder(viewType,itemView);
  7. }

小结

  • 在onCreateViewHolder中以BaseViewHolder作为返回值类型。因为BaseViewHolder作为不同类型的ViewHolder的基类,可以避免在onBindViewHolder中对ViewHolder进行类型检查与类型转换,同时也可以简化onBindViewHolder方法中的代码(具体会在下文阐述);

  • 创建不同类型的ViewHolder的相关代码被抽取到了TypeFactoryForList 中,简化了onCreateViewHolder中的代码,同时与类型相关的代码都集中在TypeFactoryForList 中,方便后期维护与拓展;

onBindViewHolder

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

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

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

 
 
  1. public class NormalViewHolder extends BaseViewHolder<Normal> {
  2.     public NormalViewHolder(View itemView) {
  3.         super(itemView);
  4.     }
  5.  
  6.     @Override
  7.     public void setUpView(final Normal model, int position, MultiTypeAdapter adapter) {
  8.         final TextView textView = (TextView) getView(R.id.normal_title);
  9.         textView.setText(model.getText());
  10.  
  11.         textView.setOnClickListener(new View.OnClickListener() {
  12.             @Override
  13.             public void onClick(View view) {
  14.                 Toast.makeText(textView.getContext(),model.getText(),Toast.LENGTH_SHORT).show();
  15.             }
  16.         });
  17.     }
  18. }

小结

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

总结

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

 
 
  1. public class MultiTypeAdapter extends RecyclerView.Adapter<BaseViewHolder> {
  2.     private TypeFactory typeFactory;
  3.     private List<Visitable> models;
  4.  
  5.     public MultiTypeAdapter(List<Visitable> models) {
  6.         this.models = models;
  7.         this.typeFactory = new TypeFactoryForList();
  8.  
  9.     }
  10.  
  11.     @Override
  12.     public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  13.         Context context = parent.getContext();
  14.  
  15.         View itemView = View.inflate(context,viewType,null);
  16.         return typeFactory.createViewHolder(viewType,itemView);
  17.     }
  18.  
  19.     @Override
  20.     public void onBindViewHolder(BaseViewHolder holder, int position) {
  21.         holder.setUpView(models.get(position),position,this);
  22.     }
  23.  
  24.     @Override
  25.     public int getItemCount() {
  26.         ifnull == models){
  27.             return 0 
  28.         }
  29.         return models.size();
  30.     }
  31.  
  32.  
  33.     @Override
  34.     public int getItemViewType(int position) {
  35.         return models.get(position).type(typeFactory);
  36.     }
  37.  
  38. }

当列表中增加类型时:

  • 为该类型创建实现了Visitable接口的Model类

  • 创建继承于BaseViewHolder的ViewHolder(与Model类对应)

  • 为TypeFactory增加type方法(与Model类对应) ,同时TypeFactoryForList 实现该方法

  • 为TypeFactoryForList增加与列表类型对应的资源ID参数

  • 修改TypeFactoryForList 中的createViewHolder方法

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

  • 拓展性——Adapter并不关心不同的列表类型在列表中的位置,因此对于Adapter来说列表类型可以随意增加或减少,我们只需要维护好数据源即可。

  • 可维护性——不同的列表类型由不同的ViewHolder维护,相互之间互不干扰;对类型的管理都在TypeFactoryForList 中,TypeFactoryForList 中的代码量少,代码简洁,维护成本低。

  • 避免了类的类型检查与类型转型,这点看源码就可以知道

源码地址

原文出处: 红灰李的简书博客 。

最后

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

当然,现今有很多技术人员,一直对官方的RecyclerView的使用及扩展延伸,乃至封装便捷性,孜孜不倦的研究着。

提供个干货:DataBindingAdapterhttps://github.com/markzhai/DataBindingAdapter

使用 Data Binding 技术的超级简单的 RecyclerView adapter,再也不需要写什么adapter了! 你也无须为此额外创建 ViewHolder 或者 ItemView 这种类。


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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值