Android-DiffUtil以及最新ListAdapter介绍

本文主要介绍Android系统中提供的工具类DiffUtil,DiffUtil的主是用与RecyclerView的局部更新,从而提高页面刷新效率。

本文基于最新的v7-27.1.1版本的RecyclerView做介绍,因为这个兼容包里面新增了一个ListAdapter,一并做介绍。

DiffUtil

DiffUtil是在安卓7.0上引入的工具类,相对于“传统”的nofityDataChange()方式:

  1. DiffUtil可以让我们做到数据局部刷新。虽然我们也可以手动去记录数据改变的位置,自己通过notifyItemRangeInserted等方法去更新数据,但是麻烦~
  2. DiffUtil可以在局部数据刷新的时候显示动画。

先上demo,看一下DiffUtil要怎么用

简易用法

public static final class MyDiffUtilCallback extends DiffUtil.Callback {

        private List<Bean> mOldList;
        private List<Bean> mNewList;

        public MyDiffUtilCallback setDates(List<Bean> oldList, List<Bean> newList) {
            mOldList = oldList;
            mNewList = newList;

            return this;
        }

        @Override
        public int getOldListSize() {
            return mOldList.size();
        }

        @Override
        public int getNewListSize() {
            return mNewList.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            return mOldList.get(oldItemPosition).id == mNewList.get(newItemPosition).id;
//            return true;
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            return mOldList.get(oldItemPosition).content.equals(mNewList.get(newItemPosition).content);
        }
    }
    
    
    ...
    
    
public void onNewData(List<Bean> newData) {                                                                    
                                                                                                               
    //数据源太大时计算费时,放入子线程                                                                                         
    //最新版本兼容包已出ListAdapter,自带子线程执行                                                                                                                                                            
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(mMyDiffUtilCallback.setDates(mBeans, newData));
        diffResult.dispatchUpdatesTo(MyAdapter.this);                                                          
        mBeans.clear();                                                                                        
        mBeans.addAll(newData);                                                                                                                                                                                      
}                                                                                                              

上面的代码很简单:

  1. 就是实现DiffUtil.Callback,指定数据差异的规则
  2. 在有新数据时,通过DiffUtil.calculateDiff(Callback cb)计算新老数据的差异
  3. 将计算完的结果更新到adapter中

我们可以看到DiffUtil.Callback主要有4个抽象方法需要实现,看一下这4个方法的具体作用。

public abstract int getOldListSize();                               
public abstract int getNewListSize();

上面2个方法主要获取新老数据的长度。

//返回值表示新数据传入时这两个位置的数据是否时同一个条目                                                                                       
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
//返回值表示新老位置的数据内容是否相同,这个方法在areItemsTheSame()返回true时生效                                                                                                                                                                                  
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);      

上面这两个方法就是区分两个数据bean是否相同。

敲黑板,这里有个注意点:

当areItemsTheSame返回为false时,不管areContentsTheSame是否为true,adapter中的条目都会更新

高级用法

DiffUtil.Callback中还有另外一个可以复写的方法

@Nullable                                                                 
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
    return null;                                                          
}                                                                         

当areItemsTheSame()返回true,同时areContentsTheSame()返回false时,通过这个方法可以用来返回具体的数据差异。

我们来看一下用法

@Override                                                                                      
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {                     
    return mOldList.get(oldItemPosition).id == mNewList.get(newItemPosition).id;               
      return true;                                                                             
}                                                                                              
                                                                                               
@Override                                                                                      
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {                  
    return false;
}                                                                                              
                                                                                               
@Nullable                                                                                      
@Override                                                                                      
public Object getChangePayload(int oldItemPosition, int newItemPosition) {                     
    Bean oldBean = mOldList.get(oldItemPosition);                                              
    Bean newBean = mNewList.get(newItemPosition);                                              
                                                                                               
    Bundle bundle = new Bundle();                                                              
                                                                                               
    if (!oldBean.content.equals(newBean.content)) {                                            
        bundle.putString("content", newBean.content);                                          
    }                                                                                          
                                                                                               
    if (bundle.size() > 0) {                                                                   
        return bundle;                                                                         
    }                                                                                          
                                                                                               
    return null;                                                                               
}                                                                                              

然后在Recycler的Adapter中我们通过onBindViewHolder()的另一个重载方法获取到之前设置的数据进行局部刷新。

@Override                                                                                        
public void onBindViewHolder(MyAdapter.ViewHolder holder, int position, @NonNull List payloads) {
                                                                                                 
    if (payloads.isEmpty()) {                                                                    
        super.onBindViewHolder(holder, position, payloads); 
        //内部其实就是调用的全部刷新的方法                                     
        //onBindViewHolder(holder, position);                                                    
    } else {                                                                                     
        Bundle bundle = (Bundle) payloads.get(0);                                                
        for (String key : bundle.keySet()) {                                                     
            if (key.equals("content")) {                                                         
                holder.contentView.setText(bundle.getString("content"));                         
            }                                                                                    
        }                                                                                        
    }                                                                                            
}                                                                                                

好了,这里就是DiffUtil的用法了,实现其中的Callback用户比较数据的差异,当来新数据时,调用其中的calculateDiff()静态方法计算数据差异即可。如果你想实现RecyclerView的局部刷新,那可以实现getChangePayload。

ListAdapter

当数据量不大时,我们可以在UI线程中直接更新数据,但是当数据量大时这就比较尴尬了,我们需要自己放在子线程操作,然后再回UI线程更新页面。

在7.0上引入DiffUtil之后,现在又在最新的v7的27.1.1兼容包中加入的官方的支持,就是ListAdapter。

public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    private final AsyncListDiffer<T> mHelper;

    @SuppressWarnings("unused")
    protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mHelper = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
                new AsyncDifferConfig.Builder<>(diffCallback).build());
    }

    @SuppressWarnings("unused")
    protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
        mHelper = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
    }

    /**
     * Submits a new list to be diffed, and displayed.
     * <p>
     * If a list is already being displayed, a diff will be computed on a background thread, which
     * will dispatch Adapter.notifyItem events on the main thread.
     *
     * @param list The new list to be displayed.
     */
    @SuppressWarnings("WeakerAccess")
    public void submitList(List<T> list) {
        mHelper.submitList(list);
    }

    @SuppressWarnings("unused")
    protected T getItem(int position) {
        return mHelper.getCurrentList().get(position);
    }

    @Override
    public int getItemCount() {
        return mHelper.getCurrentList().size();
    }
}

ListAdapter用法就跟普通的RecyclerView并无太大差别。

  1. 一共2个构造方法,区别在于第二个构造方法可以自己指定执行的线程。
  2. submitList(),用于提交新数据,更新UI。
  3. 数据比较操作的线程操作在AsyncListDiffer中实现,代码也比较简单,这里就不再贴代码分析了。需要注意点一点时,通过AsyncListDiffer返回的List是一个UnmodifiableList,意味着不能改变长度。

总结时间

  1. DiffUtil提供了梅耶斯算法实现的新老数据比较的方法,数据比较的规则在Callback
    中让用户自行实现,同时实现getChangePayload配合Adaper中的onBindViewHolder(MyAdapter.ViewHolder holder, int position, @NonNull List payloads)可以实现数据的局部更新。
  2. ListAdapter是对RecyclerView传统Adapter的一个拓展,在子线程中利用DiffUtil比较数据,并在UI线程更新。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,520评论 25 707
  • 【想法是身体的活动,还是灵魂的活动?】 如果想只是灵魂的活动, 为什么想多了头会痛, 想久了人会饿, 不想了人会迟...
    巴木驴阅读 270评论 0 0
  • 歌声,是多么的神奇, 它不需要叙述故事给我们听, 只要听着曲子的旋律, 就知道作者或歌所要表达的情感。 为丰富同学...
    西南林业大学阅读 262评论 0 0
  • 作者: 赵志敏 上到山顶,志伟电话联系接到了我们。山顶好阔。很大一个停车场。有一牌坊。左边是“轩辕庙”国务院200...
    快乐的人ZZM阅读 1,347评论 0 3
  • 父亲是一座静寞的山, 咽下了生活的苦痛, 扛起了家庭的重担, 他人未老 背已佝偻, 他年未老, 已霜满头, 生活给...
    大漠走雨阅读 189评论 0 0