在Android開(kāi)發(fā)中,常常使用含列表的UI,基本選擇RecyclerView做為列表控件。針對(duì)列表刷新簡(jiǎn)化,Google提供了DiffUtil工具,根據(jù)數(shù)據(jù)的變化指定性的更新UI。開(kāi)發(fā)者不再需要查找需要更新的是哪個(gè)Item。
但是DiffUtil提供了判斷的接口,需要開(kāi)發(fā)者自行根據(jù)自己的item Model來(lái)實(shí)現(xiàn)判斷依據(jù)。用過(guò)的小伙伴也許發(fā)現(xiàn)了,這個(gè)用起來(lái)似乎比較麻煩,特別是在列表多類(lèi)型Item的情況下,問(wèn)題將變得費(fèi)勁。本文將采用APT技術(shù)讓判斷變得簡(jiǎn)單。
如果不想了解原理,可以跳過(guò)方案設(shè)計(jì),只看解決思路和如何使用。
下面先簡(jiǎn)單介紹一下 DiffUtil 相關(guān)的:
小葵花課堂
DiffUtil 是 androidx.recyclerview.widget 下的一個(gè)工具類(lèi),一般我們不需要直接使用它,而是使用封裝好的androidx.recyclerview.widget.ListAdapter,內(nèi)部使用了DiffUtil的功能。開(kāi)發(fā)者需要繼承并實(shí)現(xiàn)DiffUtil.ItemCallback 抽象類(lèi)的三個(gè)方法:
public abstract static class ItemCallback<T> {
// 判斷是不是同一個(gè)Item,注意不是指同一類(lèi)型,而是同一行數(shù)據(jù)。
public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
// 當(dāng)areItemsTheSame返回true時(shí),判斷item的內(nèi)容是否相同。
public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
// 當(dāng)areContentsTheSame返回false時(shí),找出變化的東西并返回它。
// 返回的“東西”將在RecyclerView的方法:
// onBindViewHolder(holder: BindingViewHolder, position: Int, payloads: MutableList<Any>)
// 第三個(gè)參數(shù)里面??梢宰鯥tem里面局部刷新。
public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) {
return null;
}
}
根據(jù)areItemsTheSame或areContentsTheSame返回值,自動(dòng)判斷需要刷新的Item。(其他用法自行查閱資料。)
如果你的列表是多種類(lèi)型的Model,我想實(shí)現(xiàn)這些方法會(huì)比較麻煩吧。并且有個(gè)大問(wèn)題,如果oldItem和newItem是同一個(gè)對(duì)象,那么怎么比較都是一樣的,可是按業(yè)務(wù)邏輯上講oldItem數(shù)據(jù)展示到UI上后,自己改變了數(shù)據(jù),按理需要刷新UI的。找出變化的“東西”并能拿出來(lái)使用也不方便。

正如前文提到的,將采用APT去解決它。
APT(Annotation Processing Tool)即注解處理器,是一種處理注解的工具,確切的說(shuō)它是javac的一個(gè)工具,它用來(lái)在編譯時(shí)掃描和處理注解。注解處理器以Java代碼(或者編譯過(guò)的字節(jié)碼)作為輸入,生成.java文件作為輸出。
可以簡(jiǎn)單理解為根據(jù)注解,在編譯期生成Java代碼。
解決思路
基本思路:給Model做為比較依據(jù)的屬性簡(jiǎn)單的加個(gè)注解,比較的地方地方自動(dòng)處理。使用APT根據(jù)注解在編譯器生成輔助的副本類(lèi)(并包含比較的方法),oldItem將會(huì)有一份副本保存舊的數(shù)據(jù),用來(lái)跟newItem比較。
方案設(shè)計(jì)
幾個(gè)問(wèn)題
Q1:比較依據(jù)種類(lèi)有幾種?
A1:兩種。比較Item和比較Content。定義兩個(gè)注解:@SameItem,@SameContent。Q2:如果Model有繼承關(guān)系,父類(lèi)的依據(jù)在子類(lèi)是否可用?(比如聊天列表各種類(lèi)型繼承基類(lèi))
A2:可用。(難點(diǎn)1,需要查找父類(lèi))-
Q3:如果Model的屬性也是個(gè)Model,并且里面有判斷依據(jù),是否穿透到內(nèi)部去判斷?(比如消息里的User也是個(gè)Model)
A3:支持。新定義注解@SameType表示這個(gè)屬性需要穿透。(難點(diǎn)2,不能產(chǎn)生穿透回路,不然會(huì)形成無(wú)限遞歸,編譯期檢查出來(lái)。不能產(chǎn)生無(wú)效的@SameType注解的屬性。)
只能單向的引用鏈
Q3.1: 什么是無(wú)效的@SameType注解的屬性?
A3.1:該屬性的類(lèi)型或其父類(lèi)內(nèi)部必須至少含有@SameItem,@SameContent,@SameType其中一個(gè),@SameType引用鏈最后那個(gè)類(lèi)型必須至少含@SameItem,@SameContent其中一個(gè)。(處理起來(lái)最復(fù)雜的問(wèn)題) Q4:生成的副本是什么樣子的?
A4:副本類(lèi)只會(huì)有注解標(biāo)記的屬性。具體后面講。Q5:每個(gè)Model都有存副本嗎?那數(shù)據(jù)量會(huì)產(chǎn)生2倍哎。
Q5:不是,只有bind到ViewHolder上的Model才有副本,因?yàn)橹挥羞@些是需要判斷來(lái)刷新的。也就是說(shuō),副本的個(gè)數(shù)=ViewHolder的個(gè)數(shù)。
設(shè)計(jì)分析
根據(jù)上面問(wèn)答,需定義3個(gè)注解:@SameItem ,@SameContent , @SameType。根據(jù)3個(gè)注解的含義,我們得出三種不需要共存(不能同時(shí)作用在同一個(gè)屬性上)否則就重復(fù)判斷了。
| 注解 | 作用對(duì)象 | 含義 | value |
|---|---|---|---|
@SameItem |
屬性 | 判斷同一個(gè)Item的依據(jù) | 無(wú) |
@SameContent |
屬性 | 判斷內(nèi)容的依據(jù) | payload 的key |
@SameType |
屬性 | 穿透屬性,同時(shí)去判斷對(duì)象內(nèi)的屬性 | payload 的key |
支持繼承的話,如果子類(lèi)Model沒(méi)有這些注解,而父類(lèi)有注解,那么這個(gè)類(lèi)判斷時(shí)用的是 “最近”的父類(lèi)的副本類(lèi)。Model副本類(lèi)的繼承關(guān)系和Model的繼承關(guān)系大致一致(繼承鏈中可以空掉幾個(gè)沒(méi)注解的類(lèi))。
支持屬性穿透,如果XModol里有個(gè)屬性y(類(lèi)型為YModel)需要穿透,那么這個(gè)XModel的副本類(lèi)有一個(gè)屬性y 是YModel的副本類(lèi)類(lèi)型。

假如幾個(gè)Model體型長(zhǎng)這樣:
public class XxModel {
@SameItem
public long id;
@SameContent
public String name;
// 這個(gè)屬性沒(méi)注解,副本類(lèi)里就沒(méi)這個(gè)。
public int count;
@SameContent
public boolean valid;
@SameType
public YyModel yy;
}
// 穿透的屬性類(lèi)型
public class YyModel {
@SameItem
public long id;
@SameContent
public String title;
// //這里不能這樣用哦,否則就跟XxModel產(chǎn)生回環(huán)了。
// @SameType()
// public XxModel xx;
}
// 繼承的類(lèi)型
public class ZzModel extends XxModel {
@SameContent
public boolean zzz;
}
期望對(duì)應(yīng)生成的副本類(lèi)代碼如下:
// XxModel的副本類(lèi)
public class XxModel$$Diff$$Model implements IDiffModelType {
private long id;
private int count;
private boolean valid;
private String name;
private YyModel$$Diff$$Model yy = new YyModel$$Diff$$Model();
....省略輔助方法....
}
// YyModel的副本類(lèi)
public class YyModel$$Diff$$Model implements IDiffModelType {
private long id;
private String title;
....省略輔助方法....
}
// ZzModel的副本類(lèi),繼承XxModel的副本類(lèi)
public class ZzModel$$Diff$$Model extends XxModel$$Diff$$Model {
private boolean zzz;
....省略輔助方法....
}
有了副本類(lèi)的結(jié)構(gòu),需要怎么用呢?定義幾個(gè)輔助方法在他們的共同接口里面。
public interface IDiffModelType {
// 統(tǒng)計(jì)sameItem判斷依據(jù)的個(gè)數(shù),包括父類(lèi)的和穿透屬性的
int sameItemCount();
// 統(tǒng)計(jì)sameContent判斷依據(jù)的個(gè)數(shù),包括父類(lèi)的和穿透屬性的
int sameContentCount();
// 當(dāng)前副本與傳入數(shù)據(jù)是否同一個(gè)Item,對(duì)各@SameItem屬性判斷Objects.equals(this.attr,model.attr)
boolean isSameItem(Object o);
// 當(dāng)前副本與傳入數(shù)據(jù)是否內(nèi)容相同,對(duì)各@SameIContent屬性判斷Objects.equals(this.attr,model.attr)
boolean isSameContent(Object o);
// 當(dāng)前副本是否可以處理傳入的對(duì)象的類(lèi)型
boolean canHandle(Object o);
// 從傳入的對(duì)象上獲取屬性值,即記錄副本
void from(Object o);
// 找出傳入對(duì)象與副本不一樣的值,Payload 內(nèi)部是一個(gè)map存變化了的數(shù)據(jù)。
Payload payload(Object o);
}
這些方法計(jì)算的時(shí)候,都會(huì)調(diào)用其父類(lèi)的和穿透屬性的同名方法結(jié)合計(jì)算結(jié)果(除了canHandle)。后面把sameItemCount和sameContentCount也優(yōu)化掉了,只返回具體數(shù)據(jù),因?yàn)樵诰幾g期間就可以計(jì)算出具體的數(shù)值了。
如何創(chuàng)建這些副本類(lèi)?再自動(dòng)生成一些工廠類(lèi)和獲取工廠的類(lèi)即可。(注意,根據(jù)類(lèi)型判斷時(shí),子類(lèi)的放父類(lèi)的前面)。
APT實(shí)現(xiàn)
這部分代碼比較長(zhǎng)。。。
主要針對(duì)QA中的幾個(gè)難點(diǎn)需要做一些數(shù)據(jù)結(jié)構(gòu)的復(fù)雜邏輯。(看源碼吧,有注釋?zhuān)?/p>
需要注意一點(diǎn),通過(guò)Model給副本賦值時(shí),java的話屬性是public的,直接賦值即可。但是如果Model是kotlin代碼,看上去public的屬性,實(shí)際上會(huì)變成private屬性,然后生成GET方法。
private String spellGetFunction(VariableElement element) {
if (element.getModifiers().contains(Modifier.PUBLIC)) {
return element.getSimpleName().toString();
} else {
String name = element.getSimpleName().toString();
// boolean或者Boolean類(lèi)型的話,如果is開(kāi)頭,第3個(gè)字母不是小寫(xiě)的話,特殊處理。
if (element.asType().getKind() == TypeKind.BOOLEAN
|| element.asType().toString().equalsIgnoreCase(BOOLEAN_TYPE)) {
byte[] items = name.getBytes();
if (items.length >= 3) {
char c0 = (char) items[0];
char c1 = (char) items[1];
char c2 = (char) items[2];
if (c0 == 'i' && c1 == 's' && (c2 < 'a' || c2 > 'z')) {
return name + "()";
}
}
}
// get+屬性名首字母變大寫(xiě)+()
return "get" + toUpper(name) + "()";
}
}
寫(xiě)框架,雖然很麻煩,如果用起來(lái)就值得了。
對(duì)外封裝
APT生成了副本類(lèi),工廠類(lèi),獲取工廠的類(lèi)。剩下的還需要封裝使用這些類(lèi)。對(duì)外提供一個(gè)Helper類(lèi)。
public final class DiffModelHelper {
private static class Data {
Object model;
IDiffModelType diff;
}
// 保存 數(shù)據(jù)與綁定對(duì)象(一般時(shí)ViewHolder 或者它的itemView)
private final Map<Object, Data> bindMap = new WeakHashMap<>();
private boolean byObjectsEquals = true;
/**
* 沒(méi)有依據(jù)時(shí)是否用 {@link Objects#equals(Object, Object)} 來(lái)判斷是否同一行。
*/
public synchronized void isSameItemByObjectsEquals(boolean use) {
this.byObjectsEquals = use;
}
/**
* 新舊數(shù)據(jù)內(nèi)容是否同一行。
*/
public synchronized boolean isSameItem(@NonNull Object oldModel, @NonNull Object newModel) {
IDiffModelType diff = findDiff(oldModel);
if (diff == null) return false;
if (diff.canHandle(newModel) && diff.sameItemCount() > 0) {
return diff.isSameItem(newModel);
}
return byObjectsEquals && Objects.equals(oldModel, newModel);
}
/**
* 新舊數(shù)據(jù)內(nèi)容是否相同。
*/
public synchronized boolean isSameContent(@NonNull Object oldModel, @NonNull Object newModel) {
IDiffModelType diff = findDiff(oldModel);
if (diff == null) return false;
if (diff.canHandle(newModel) && diff.sameContentCount() > 0) {
return diff.isSameContent(newModel);
}
return false;
}
/**
* 獲取改變的差異。
*/
@Nullable
public synchronized Payload getPayload(@NonNull Object oldModel, @NonNull Object newModel) {
IDiffModelType diff = findDiff(oldModel);
if (diff == null) return null;
if (diff.canHandle(newModel) && diff.sameContentCount() > 0) {
return diff.payload(newModel);
}
return null;
}
/**
* 綁定上新的數(shù)據(jù)。
*/
public synchronized void bindNewData(@NonNull Object bindObj, @NonNull Object newModel) {
Data data = bindMap.get(bindObj);
if (data != null && data.diff.canHandle(newModel)) {
data.diff.from(newModel);
return;
}
IDiffModelType diff = tryCreateDiff(newModel);
if (diff == null) return;
diff.from(newModel);
data = new Data();
data.model = newModel;
data.diff = diff;
bindMap.put(bindObj, data);
}
@Nullable
private IDiffModelType findDiff(@NonNull Object model) {
for (Data data : bindMap.values()) {
if (data.model == model) return data.diff;
}
return null;
}
@Nullable
private IDiffModelType tryCreateDiff(@NonNull Object model) {
IDiffModelFactory factory = DiffModelFactoryManager.getInstance().getFactory(model);
if (factory != null) return factory.create();
return null;
}
}
其中DiffModelFactoryManager是管理工廠的單例(內(nèi)部用來(lái)LruCache對(duì)工廠做了緩存)。
如何使用
目前已經(jīng)發(fā)布到 JitPack。
添加依賴(lài)
我項(xiàng)目里用了.gradle.kts。gradle類(lèi)似,跟常規(guī)依賴(lài)差不多。
Project.build.gradle.kts:
allprojects {
repositories {
google()
jcenter()
maven { url = uri("https://jitpack.io") }// add this line.
}
}
app.build.gradle.kts:( lastVersion見(jiàn)github)
implementation("com.github.wzmyyj.FeDiff:lib_diff_api:lastVersion")
// or kotlin use kapt
annotationProcessor("com.github.wzmyyj.FeDiff:lib_diff_compiler:lastVersion")
代碼
初始化:在Application里。
FeDiff.init(this, true)// 第二個(gè)參數(shù)表示是否debug
可以結(jié)合ListAdapter定義一個(gè)DiffUtil.ItemCallback<M>。例如:
class DiffModelCallback<M : IVhModelType> : DiffUtil.ItemCallback<M>() {
private val helper = DiffModelHelper()
fun getHelper(): DiffModelHelper = helper
fun bindNewData(bindObj: Any, newModel: M) {
helper.bindNewData(bindObj, newModel)
}
override fun areItemsTheSame(oldItem: M, newItem: M): Boolean {
return helper.isSameItem(oldItem, newItem)
}
override fun areContentsTheSame(oldItem: M, newItem: M): Boolean {
return helper.isSameContent(oldItem, newItem)
}
override fun getChangePayload(oldItem: M, newItem: M): Any? {
// return null; //如果不做Item局部刷新就返回 null。
return helper.getPayload(oldItem, newItem)
}
}
然后在適配器里:
override fun onBindViewHolder(holder: BindingViewHolder, position: Int, payloads: MutableList<Any>) {
val payload = payloads.firstOrNull() as? Payload
if (payload != null && payload.isEmpty.not()) {// 如果不做Item里局部刷新可以不需要這幾行。
// 根據(jù).payload做Item里局部刷新。
val newAttr = payload.getString("key", "xxx")
holder.itemView.tv.text = newAttr
} else {
super.onBindViewHolder(holder, position, payloads)
}
// 最后給副本綁定新的數(shù)據(jù)。這行必須加!
callback.bindNewData(holder, getItem(position))
// or callback.bindNewData(holder.itemView, getItem(position))
}
callback是上面定義的DiffModelCallback 。然后在你的model里加注解即可,例如:
class MsgModel {
// 判斷同一個(gè)Item的依據(jù)
@SameItem
var id: Long = 0
// 判斷內(nèi)容的依據(jù)
@SameContent
var content: String? = null
// 判斷內(nèi)容的依據(jù)
@SameContent
var time: Long = 0L
// 不需要判斷,不加注解
var valid = false
// 穿透屬性,會(huì)同時(shí)去判斷UserModel里面的屬性
@SameType
var user: UserModel? = null
}
class UserModel {
@SameContent
var name: String? = null
@SameContent
var avatar: String? = null
}
剩下的就是把model添加到列表上嘍。框架會(huì)幫你自動(dòng)計(jì)算變化了那些。
項(xiàng)目地址
歡迎Star或Issues。
其他
搭配這個(gè)更香!DataBinding下的RecyclerView萬(wàn)能適配器:
wzmyyj/FeAdapter
