EpoxyRecyclerView的简单使用与源码分析

时间:2024-11-17 16:31:27

前言

EpoxyRecyclerView很好地支持列表中有多种布局类型,让我们在使用的过程无需去关注ViewType,而是直接为每个类型定义对应的Model就可以了。它会自动为我们做了差分,还提高了的性能。

EpoxyRecyclerView的Github地址:

/airbnb/epoxy

EpoxyRecyclerView的使用

添加依赖:

在app的build.gradle文件中添加

apply plugin: 'kotlin-kapt'
  • 1

然后加入依赖:

implementation ':epoxy:3.7.0'
kapt ':epoxy-processor:3.7.0'
  • 1
  • 2

使用:

1.与使用RecyclerView一样,在布局文件里边加入EpoxyRecyclerView

<LinearLayout
        xmlns:android="/apk/res/android"
        xmlns:tools="/tools"
        xmlns:app="/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

    <
            android:id="@+id/epoxy_rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>


</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

2.创建一个Controller:

EpoxyController有好几个子类,可根据类型参数的个数来选择继承,以下例子继承的是一个参数的TypedEpoxyController,如果有两个参数的话可继承Typed2EpoxyController,以此类推,最高的数字为4:

class HeaderController() : TypedEpoxyController<List<String>>() {
    override fun buildModels(titles: List<String>?) {
    }
}
  • 1
  • 2
  • 3
  • 4

继承后需要实现buildModels方法,参数为我们展示需要用到的数据。

3.创建Model:

继承EpoxyModelWithHolder,并用注解EpoxyModelClass来标记这是一个Model类,然后会生成对应的类使用。Model类需要是抽象的:

@EpoxyModelClass(layout = R.layout.header_view)
abstract class HeaderModel : EpoxyModelWithHolder<Holder>(){

    @EpoxyAttribute
    lateinit var title: String

    override fun bind(holder: Holder) {
        holder.tvHeader.text =title
    }
}

class Holder :EpoxyHolder(){

    lateinit var tvHeader : TextView

    override fun bindView(itemView: View) {
        tvHeader = itemView.findViewById(R.id.tv_header)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

layout属性表示对应的布局文件。

写好Model类之后,build一下,然后就可以在controller中添加Model了,这里只用一种Model,在实际开发中可根据对应的条件添加相应的Model:

class HeaderController(private val callback: OriginHeaderModel.OnBtnCallback) : TypedEpoxyController<List<String>>() {


    override fun buildModels(titles: List<String>?) {

        originHeader {
            id("button")
            callback(callback)
        }

        titles?.forEachIndexed { index, title ->
            if (index%2==0){
                header {
                    id(title.hashCode())
                    title(title)
                }
            }else{
                headerFlase {
                    id(title.hashCode())
                    falseTitle(title)
                }
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

完成了Controller和Model后,就可以进行使用了:

需要给EpoxyRecyclerView设置Controller,当更新数据时,则直接给controller设置数据即可:

mEpoxyRecyclerView.setController(headerController)

 private fun updateController(){
        headerController.setData(mTitles)
}
  • 1
  • 2
  • 3
  • 4
  • 5

EpoxyRecyclerView原理

设置数据

mHomeMessageController.setData(mHomeMessageInfo)
  • 1

setData里边会调用requestModelBuild

public void requestModelBuild() {
  if (isBuildingModels()) {
    throw new IllegalEpoxyUsage("Cannot call `requestModelBuild` from inside `buildModels`");
  }
//当在第一次buildModels时,会立即执行,否则的话会延迟执行
//这样是为了尽可能快地将内容显示。
  if (hasBuiltModelsEver) {
    requestDelayedModelBuild(0);
  } else {
    buildModelsRunnable.run();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

buildModelsRunnable

//保留核心的代码
private final Runnable buildModelsRunnable = new Runnable() {
  @Override
  public void run() {
  //......

    modelsBeingBuilt = new ControllerModelList(getExpectedModelCount());

    try {
      buildModels();
    } catch (Throwable throwable) {
     //...
    }

    runInterceptors();
 
    (modelsBeingBuilt);
  }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

modelsBeingBuilt是一个数组列表的包装者的子类,用来存放Model,当调用freeze方法后它强制不对列表进行更改。防止拦截器存储列表并去改变它。

buildModels是一个抽象方法,留给我们去实现。

runInterceptors方法会去执行Interceptor的操作。

Adapter的setModels方法:

void setModels(@NonNull ControllerModelList models) {
  differ.submitList(models);
}
  • 1
  • 2
  • 3

differ为AsyncEpoxyDiffer对象,用来做差分

public void submitList(@Nullable final List<? extends EpoxyModel<?>> newList) {
  final int runGeneration;
  @Nullable final List<? extends EpoxyModel<?>> previousList;

  synchronized (this) {
    // Incrementing generation means any currently-running diffs are discarded when they finish
    // We synchronize to guarantee list object and generation number are in sync
    runGeneration = generationTracker.incrementAndGetNextScheduled();
    previousList = list;
  }
//什么事都不用干
  if (newList == previousList) {
    // nothing to do
    onRunCompleted(runGeneration, newList, DiffResult.noOp(previousList));
    return;
  }
//快速清除
  if (newList == null || newList.isEmpty()) {
    // fast simple clear all
    DiffResult result = null;
    if (previousList != null && !previousList.isEmpty()) {
      result = DiffResult.clear(previousList);
    }
    onRunCompleted(runGeneration, null, result);
    return;
  }
//快速插入
  if (previousList == null || previousList.isEmpty()) {
    // fast simple first insert
    onRunCompleted(runGeneration, newList, DiffResult.inserted(newList));
    return;
  }

  final DiffCallback wrappedCallback = new DiffCallback(previousList, newList, diffCallback);

  executor.execute(new Runnable() {
    @Override
    public void run() {
      DiffUtil.DiffResult result = DiffUtil.calculateDiff(wrappedCallback);
      onRunCompleted(runGeneration, newList, DiffResult.diff(previousList, newList, result));
    }
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

DiffCallback继承了,几个重要的方法实现如下:

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
  return diffCallback.areItemsTheSame(
      oldList.get(oldItemPosition),
      newList.get(newItemPosition)
  );
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
  return diffCallback.areContentsTheSame(
      oldList.get(oldItemPosition),
      newList.get(newItemPosition)
  );
}

@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
  return diffCallback.getChangePayload(
      oldList.get(oldItemPosition),
      newList.get(newItemPosition)
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

diffCallback是一个ItemCallback<EpoxyModel<?>>对象,它的实现如下:

private static final ItemCallback<EpoxyModel<?>> ITEM_CALLBACK =
    new ItemCallback<EpoxyModel<?>>() {
      @Override
      public boolean areItemsTheSame(EpoxyModel<?> oldItem, EpoxyModel<?> newItem) {
        return oldItem.id() == newItem.id();
      }

      @Override
      public boolean areContentsTheSame(EpoxyModel<?> oldItem, EpoxyModel<?> newItem) {
        return oldItem.equals(newItem);
      }

      @Override
      public Object getChangePayload(EpoxyModel<?> oldItem, EpoxyModel<?> newItem) {
        return new DiffPayload(oldItem);
      }
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

onRunCompleted方法:

private void onRunCompleted(
    final int runGeneration,
    @Nullable final List<? extends EpoxyModel<?>> newList,
    @Nullable final DiffResult result
) {

  // We use an asynchronous handler so that the Runnable can be posted directly back to the main
  // thread without waiting on view invalidation synchronization.
  MainThreadExecutor.ASYNC_INSTANCE.execute(new Runnable() {
    @Override
    public void run() {
      final boolean dispatchResult = tryLatchList(newList, runGeneration);
      if (result != null && dispatchResult) {
        resultCallack.onResult(result);
      }
    }
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在主线程调度一个任务,onResult方法:

@Override
public void onResult(@NonNull DiffResult result) {
  itemCount = result.newModels.size();
  notifyBlocker.allowChanges();
  result.dispatchTo(this);
  notifyBlocker.blockChanges();
//...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

dispatchTo方法:

public void dispatchTo(Adapter adapter) {
  dispatchTo(new AdapterListUpdateCallback(adapter));
}
  • 1
  • 2
  • 3
public void dispatchTo(ListUpdateCallback callback) {
  if (differResult != null) {
    differResult.dispatchUpdatesTo(callback);
  } else if (newModels.isEmpty() && !previousModels.isEmpty()) {
    callback.onRemoved(0, previousModels.size());
  } else if (!newModels.isEmpty() && previousModels.isEmpty()) {
    callback.onInserted(0, newModels.size());
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

后面几个流程其实是自动为我们实现差分计算。

Epoxy生成的类,为我们实现了equals和hashCode方法,用于差分时使用。

Model的调用

BaseEpoxyAdapter实现了:

在onCreateViewHolder方法,会创建一个EpoxyViewHolder对象

在onBindViewHolder方法中,会调用EpoxyViewHolder对象的bind方法:

public void bind(@SuppressWarnings("rawtypes") EpoxyModel model,
    @Nullable EpoxyModel<?> previouslyBoundModel, List<Object> payloads, int position) {
  this.payloads = payloads;
//创建Holder,这在Epoxy生成的代码中有提供创建对应的Holder
  if (epoxyHolder == null && model instanceof EpoxyModelWithHolder) {
    epoxyHolder = ((EpoxyModelWithHolder) model).createNewHolder();
    epoxyHolder.bindView(itemView);
  }

  //...model的bind前操作

  if (previouslyBoundModel != null) {
    //提供之前与holder绑定的Model,默认会调用bind(T view方法)
    model.bind(objectToBind(), previouslyBoundModel);
  } else if (payloads.isEmpty()) {
    //将数据绑定给视图
    model.bind(objectToBind());
  } else {
    // 不是全量更新时调用,默认会调用bind(T view方法)
    model.bind(objectToBind(), payloads);
  }

//...绑定后的操作

  epoxyModel = model;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

注意的点

使用同一布局文件:

 java.lang.ClassCastException: com.sendi.epoxydemo.Holder cannot be cast to com.sendi.epoxydemo.HeaderFlaseModel$FalseHolder
        at com.sendi.epoxydemo.HeaderFlaseModel_.handlePreBind(HeaderFlaseModel_.java:22)
  • 1
  • 2

BaseEpoxyAdapter的getItemViewType实现寄托给viewTypeManager,

最终会调用getViewType静态方法:

static int getViewType(EpoxyModel<?> model) {
  int defaultViewType = model.getViewType();
  if (defaultViewType != 0) {
    return defaultViewType;
  }

  Class modelClass = model.getClass();

  Integer viewType = VIEW_TYPE_MAP.get(modelClass);

  if (viewType == null) {
    viewType = -VIEW_TYPE_MAP.size() - 1;
    VIEW_TYPE_MAP.put(modelClass, viewType);
  }

  return viewType;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

首先是使用model的getViewType方法来获取的:

protected int getViewType() {
  return getLayout();
}
  • 1
  • 2
  • 3

此方法默认是返回布局id的值,所以当使用同一布局文件时,导致在复用的时候,返回之前的EpoxyViewHolder,它里边持有EpoxyHolder,也就是我们手动实现的Holder,按道理来说,如果不同类型的Model则会重新创建,但由于用相同的布局文件,导致返回发viewType相同,最终复用了不同Model的EpoxyViewHolder,由于调用过bind,所以epoxyHolder不会重新创建,进而导致Holder类型转换错误

结语

EpoxyRecyclerView对于多类型Item的列表,使用起来确实很方便,推荐可以试着使用。本次的分享到这里结束了,有不妥之处欢迎指正或讨论。