【Android】你可能不知道的Support(一) 0步自动定向刷新SortedList

转载请标明出处: http://www.jianshu.com/p/b888d6d17b5a
本文出自:【张旭童的简书】 (http://www.jianshu.com/users/8e91ff99b072/latest_articles)
代码传送门:喜欢的话,随手点个star。多谢
https://github.com/mcxtzhang/SupportDemos

背景:

打算写一个系列了,讲解Android Support包内那些常用or冷门有用的工具类的合集。

最近leader在优化IM会话列表,同事以前的做法是无脑notifyDatasetChanged()刷新RecyclerView的。
在消息聊得很嗨很多的时候,界面频繁刷新,会话列表会出现丢失焦点现象。且性能毕竟不高。
遂想采用定向刷新

同事知道我以前研究过DiffUtil和定向刷新相关内容,于是便和我讨论。

(不知道DiffUtil的点这里)http://www.jianshu.com/p/9b6e12d8eea0

(不了解定向刷新的点这里)http://www.jianshu.com/p/1ac13f74da63

由于IM会话列表是从数据库里读的,他还告诉我会有数据集重复的现象,且会话列表肯定是按时间排序的,所以这对我们的数据组织提出了两点要求:有序、去重

我的想法是:

  • 采用DiffUtil自动计算新老数据集差异,然后自动完成定向刷新
  • 至于数据集的去重和有序,我打算用TreeSet去帮助我们做。

利用Set本身元素不重复的特性,加之Tree的有序性,来解决数据组织的两个需求。

可是leader不知道从哪搜出来一个SortedList,告诉我这是Android SDK提供的。也可以完成排序and去重。
我心说这是哪路神仙,我以为是JDK给的呢,于是也查阅了一番资料,遂揭开了SortedList的神秘面纱,也有了今天的文章。

转载请标明出处: http://www.jianshu.com/p/b888d6d17b5a
本文出自:【张旭童的简书】 (http://www.jianshu.com/users/8e91ff99b072/latest_articles)
代码传送门:喜欢的话,随手点个star。多谢
https://github.com/mcxtzhang/SupportDemos

SortedList是什么?

源码头注释如下:

A Sorted list implementation that can keep items in order and also notify for changes in the list。

翻译:
一个有序列表(数据集)的实现,可以保持ItemData都是有序的,并(自动)通知列表(RecyclerView)(数据集)中的更改。

人话:
首先它是一种数据结构,是一个有序的List,list改变后(增删改查),也可以一直保持数据集List有序,并且会自动调用adapter中定向更新notifyXXXX方法,更新RecyclerView。
对了,它还会自动去重

关键点:
搭配RecyclerView使用,去重,有序,自动定向刷新

刚看到这里,我觉得这特么自动定向刷新这一点特性,怎么有点像DiffUtil,后来我查阅资料才发现,这家伙出来的比DiffUtil要早,是在Support Library 22 引入的。所以说应该是DiffUtil像它。

而且SortedList 和 DiffUtil 内部 都实现、持有了一些共同的接口,暴漏出供我们重写比较规则的Callback的方法名都几乎一毛一样。

我个人感觉SortedList从设计上和DiffUtil比,是有一点点不足,这可能也是官方后来又在Support Library 24 中引入DiffUtil的一个理由吧。具体异同,稍后总结。先看怎么用吧。

Demo效果图,据说没图的文章没人看
Demo效果图,据说没图的文章没人看

用法:

我们来看看如果使用SortedList该怎么写:

Adapter:

要写RecyclerView,就少不了Adapter。
一个常规的Adapter内部一般持有一个List<T>的数据集,
使用SortedList的话,需要将存储数据源的变量类型改变成SortedList,

  • 唯一差异:将以前的ArrayList->替换为SortedList.

其他的话,倒没有变化,因为SortedList虽然没有继承自List,但是暴漏出API还和List一样的。

public class SortedAdapter extends RecyclerView.Adapter<SortedAdapter.VH> {
    /**
     * 数据源替换为SortedList,
     * 以前可能会用ArrayList。
     */
    private SortedList<TestSortBean> mDatas;
    ...

    public SortedAdapter(Context mContext, SortedList<TestSortBean> mDatas) {
        this.mContext = mContext;
        this.mDatas = mDatas;
        mInflater = LayoutInflater.from(mContext);
    }

    public void setDatas(SortedList<TestSortBean> mDatas) {
        this.mDatas = mDatas;
    }

    @Override
    public SortedAdapter.VH onCreateViewHolder(ViewGroup parent, int viewType) {
        return new SortedAdapter.VH(mInflater.inflate(R.layout.item_diff, parent, false));
    }

    @Override
    public void onBindViewHolder(final SortedAdapter.VH holder, final int position) {
        TestSortBean bean = mDatas.get(position);
        holder.tv1.setText(bean.getName());
        holder.tv2.setText(bean.getId() + "");
        holder.iv.setImageResource(bean.getIcon());
    }
    ...
}

实体类

无任何修改,就是一个普通的实体类。与上文DiffUtil里的一样。

Callback:

看过DiffUtil详解的同学对这个Callback的编写和理解就易如反掌了,编写规则和套路和DiffUtil.Callback一样。
而且还少写一个方法public Object getChangePayload(int oldItemPosition, int newItemPosition),这里顺带复习一下上文内容,这个方法返回 一个 代表着新老item的改变内容的 payload对象
这里说远一点,关于这个少写的方法,正是定向刷新中部分绑定(Partial bind)的核心方法。
DiffUtil是利用这个getChangePayload()方法的返回值,作为第三个参数,回调ListUpdateCallback接口里的void onChanged(int position, int count, Object payload);方法,最终回调adapter.notifyItemRangeChanged(position, count, payload);方法,再往下就走到Adapter的三参数onBindViewHolder(VH holder, int position, List<Object> payloads)方法,也就是我们部分绑定所操作的地方了,不太明白的可以去看DiffUtil详解.
除此之外,耶不用传新旧数据集进来了,里面的每个方法都是直接传入ItemData进行比较。
那么我们的SortedList的Callback如下编写:

public class SortedListCallback extends SortedListAdapterCallback<TestSortBean> {
    public SortedListCallback(RecyclerView.Adapter adapter) {
        super(adapter);
    }

    /**
     * 把它当成equals 方法就好
     */
    @Override
    public int compare(TestSortBean o1, TestSortBean o2) {
        return o1.getId() - o2.getId();
    }

    /**
     * 和DiffUtil方法一致,用来判断 两个对象是否是相同的Item。
     */
    @Override
    public boolean areItemsTheSame(TestSortBean item1, TestSortBean item2) {
        return item1.getId() == item2.getId();
    }
    /**
     * 和DiffUtil方法一致,返回false,代表Item内容改变。会回调mCallback.onChanged()方法;
     */
    @Override
    public boolean areContentsTheSame(TestSortBean oldItem, TestSortBean newItem) {
        //默认相同 有一个不同就是不同
        if (oldItem.getId() != newItem.getId()) {
            return false;
        }
        if (oldItem.getName().equals(newItem.getName())) {
            return false;
        }
        if (oldItem.getIcon() != newItem.getIcon()) {
            return false;
        }
        return true;
    }
}

Activity:

Activity的编写也没啥大变化,区别如下:

  • 以前构建Adapter时,一般会将data也一起传入,现在可传可不传。
  • SortedList初始化的时候,要将Adapter传进来。所以先构建Adapter,再构建SortedList
public class SortedListActivity extends AppCompatActivity {
    /**
     * 数据源替换为SortedList,
     * 以前可能会用ArrayList。
     */
    private SortedList<TestSortBean> mDatas;
    private RecyclerView mRv;
    private SortedAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sorted_list);

        mRv = (RecyclerView) findViewById(R.id.rv);
        mRv.setLayoutManager(new LinearLayoutManager(this));
        //★以前构建Adapter时,一般会将data也一起传入,现在有变化
        mAdapter = new SortedAdapter(this, null);
        mRv.setAdapter(mAdapter);
        initData();
        mAdapter.setDatas(mDatas);

    }

    private void initData() {
        //★SortedList初始化的时候,要将Adapter传进来。所以先构建Adapter,再构建SortedList
        mDatas = new SortedList<>(TestSortBean.class, new SortedListCallback(mAdapter));
        mDatas.add(new TestSortBean(10, "Android", R.drawable.pic1));
        //★注意这里有一个重复的字段 会自动去重的。
        mDatas.add(new TestSortBean(10, "Android重复", R.drawable.pic1));
        mDatas.add(new TestSortBean(2, "Java", R.drawable.pic2));
        mDatas.add(new TestSortBean(30, "背锅", R.drawable.pic3));
        mDatas.add(new TestSortBean(4, "手撕产品", R.drawable.pic4));
        mDatas.add(new TestSortBean(50, "手撕测试", R.drawable.pic5));
    }

代码写到这里,界面就可以正常显示了。效果如Gif图。
可以看到虽然我们add进去的数据 是有重复的,顺序也是乱序的。
但是列表界面依然按照id的升序显示。
到这就完了吗,还没有说到自动定向刷新呢。

0步自动定向刷新

DiffUtil两步完成定向刷新比,SortedList这一点真的是很强。0步完成自动定向刷新

新增一条:

在上述代码的基础上,如果此时查询数据库,发现有一条新的IM聊天信息,那么直接add()进来即可:
add 内部会自动调用 mCallback.onInserted(index, 1) ->notifyItemRangeInserted(index,1)
也就是说我们add一次 它就notify一次,没有batch操作,有点low

        mDatas.add(new TestSortBean(26, "温油对待产品", R.drawable.pic6));//模拟新增
        mDatas.add(new TestSortBean(12, "小马可以来点赞了", R.drawable.pic6));//模拟新增
        mDatas.add(new TestSortBean(2, "Python", R.drawable.pic6));//add进去 重复的会自动修改

新增一坨:

如果是一坨消息,可以用addAll(),查看源码,它内部会自动做Batch操作,beginBatchedUpdates();endBatchedUpdates();。所以如果想batch,就必须用addAll()操作,感觉这算一个限制。

        //addAll 也分两种
        //第一种 以可变参数addAll
        //mDatas.addAll(new TestSortBean(26, "帅", R.drawable.pic6),new TestSortBean(27, "帅", R.drawable.pic6));
        //第二种 集合形式

        List<TestSortBean> temp = new ArrayList<>();
        temp.add(new TestSortBean(26, "帅", R.drawable.pic6));
        temp.add(new TestSortBean(28, "帅", R.drawable.pic6));
        mDatas.addAll(temp);

刷新

而如果是刷新的场景,可能就不太适用了,刷新时,服务器给我们的一般都是一个List,直接addAll 要先clear, 会闪屏:

       List<TestSortBean> newDatas = new ArrayList<>();
        for (int i = 0; i < mDatas.size(); i++) {
            try {
                newDatas.add(mDatas.get(i).clone());//clone一遍旧数据 ,模拟刷新操作
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
        }
        newDatas.add(new TestSortBean(29, "帅", R.drawable.pic6));//模拟新增数据
        newDatas.get(0).setName("Android+");
        newDatas.get(0).setIcon(R.drawable.pic7);//模拟修改数据
        TestSortBean testBean = newDatas.get(1);//模拟数据位移
        newDatas.remove(testBean);
        newDatas.add(testBean);
        mDatas.clear();
        mDatas.addAll(newDatas);

异步操作

查看源码,SortedList是在每次add()addAll()clear().....等对数据集进行增删改查的函数里,都会进行一遍排序和去重。这排序和去重显然是个耗时操作。那么我想说能不能用异步处理呢?丢在子线程中。
于是我如下写:

        //每次add都会计算一次 想放在子线程中
       new Thread(new Runnable() {
            @Override
            public void run() {
                mDatas.add(new TestSortBean(26, "帅", R.drawable.pic6));//模拟新增数据
                mDatas.add(new TestSortBean(27, "帅", R.drawable.pic6));//模拟新增数据
            }
        }).start();
    }

然而这是肯定不行的,上文提过,每次add 会自动 mAdapter.notifyItemRangeInserted(position, count);
在线程中操作UI,会android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
这一点就不如DiffUtil啦。

和DiffUtil的异同

它们两真的很像,而且不管是DiffUtil计算出Diff后,还是SortedList修改过数据后,内部持有的回调接口都是同一个:android.support.v7.util.ListUpdateCallback:

/**
 * An interface that can receive Update operations that are applied to a list.
 * <p>
 * This class can be used together with DiffUtil to detect changes between two lists.
 */
public interface ListUpdateCallback {
    void onInserted(int position, int count);

    void onRemoved(int position, int count);

    void onMoved(int fromPosition, int toPosition);

    void onChanged(int position, int count, Object payload);
}

我就不解析源码了,非常简单,大致流程就是:
DiffUtil计算出Diff或者SortedList察觉出数据集有改变后,在合适的时机,回调ListUpdateCallback接口的这四个方法,DiffUtilSortedList提供的默认Callback实现中,都会通知Adapter完成定向刷新。
这就是自动定向刷新的原理。

总结一下它们的异同吧:

  • DiffUtil比较两个数据源(一般是List)的差异(Diff),Callback中比对时 传递的参数是 position
  • SortedList 能完成数据集的排序去重, Callback中比对时,传递的是ItemData (JavaBean)。
  • DiffUtil 能完成自动定向刷新 + 部分绑定
  • SortedList 只能完成自动定向刷新
  • DiffUtil 更通用,SortedList还与数据结构耦合
  • DiffUtils: 检测不出重复的,会被认为是新增的。(因为比对的核心是postion。 所以无法去重) 但是IM这种消息顺序移动会被检测到。
  • 它们都是一种自动定向刷新的手段

感受总结:

使用SortedList的话,Adapter的保存数据集的变量类型要改变。
对代码有侵入性,没有热插拔的快感。
在项目中有各种BaseAdapter的前提下,可能要扩展一种BaseSortedListAdater更方便使用。

只不过它的目的不是在定向刷新,而是维护数据集的 有序 & 去重
顺带有一个定向刷新的功能。

而DiffUtil主打的就是 比较集合的差异,更是帮我们自动完成定向刷新

所以SortedList 不适用于 服务器给所有数据过来的,下拉刷新情况。此时不如使用普通的List。

它的亮点和核心,还是在于 有序 & 去重

且它也不支持 部分绑定(Partial bind)

但它在特定场景下,例如 数据集每次更新时是增量更新,且需要维持一个排序规则的时候,就像城市列表界面,还是给我们带来了一定的便利之处的。

转载请标明出处: http://www.jianshu.com/p/b888d6d17b5a
本文出自:【张旭童的简书】 (http://www.jianshu.com/users/8e91ff99b072/latest_articles)
代码传送门:喜欢的话,随手点个star。多谢
https://github.com/mcxtzhang/SupportDemos

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容