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

轉(zhuǎn)載請(qǐng)標(biāo)明出處: http://www.itdecent.cn/p/b888d6d17b5a
本文出自:【張旭童的簡(jiǎn)書(shū)】 (http://www.itdecent.cn/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點(diǎn)個(gè)star。多謝
https://github.com/mcxtzhang/SupportDemos

背景:

打算寫(xiě)一個(gè)系列了,講解Android Support包內(nèi)那些常用or冷門有用的工具類的合集。

最近leader在優(yōu)化IM會(huì)話列表,同事以前的做法是無(wú)腦notifyDatasetChanged()刷新RecyclerView的。
在消息聊得很嗨很多的時(shí)候,界面頻繁刷新,會(huì)話列表會(huì)出現(xiàn)丟失焦點(diǎn)現(xiàn)象。且性能畢竟不高。
遂想采用定向刷新。

同事知道我以前研究過(guò)DiffUtil和定向刷新相關(guān)內(nèi)容,于是便和我討論。

(不知道DiffUtil的點(diǎn)這里)http://www.itdecent.cn/p/9b6e12d8eea0

(不了解定向刷新的點(diǎn)這里)http://www.itdecent.cn/p/1ac13f74da63

由于IM會(huì)話列表是從數(shù)據(jù)庫(kù)里讀的,他還告訴我會(huì)有數(shù)據(jù)集重復(fù)的現(xiàn)象,且會(huì)話列表肯定是按時(shí)間排序的,所以這對(duì)我們的數(shù)據(jù)組織提出了兩點(diǎn)要求:有序、去重

我的想法是:

  • 采用DiffUtil自動(dòng)計(jì)算新老數(shù)據(jù)集差異,然后自動(dòng)完成定向刷新
  • 至于數(shù)據(jù)集的去重和有序,我打算用TreeSet去幫助我們做。

利用Set本身元素不重復(fù)的特性,加之Tree的有序性,來(lái)解決數(shù)據(jù)組織的兩個(gè)需求。

可是leader不知道從哪搜出來(lái)一個(gè)SortedList,告訴我這是Android SDK提供的。也可以完成排序and去重。
我心說(shuō)這是哪路神仙,我以為是JDK給的呢,于是也查閱了一番資料,遂揭開(kāi)了SortedList的神秘面紗,也有了今天的文章。

轉(zhuǎn)載請(qǐng)標(biāo)明出處: http://www.itdecent.cn/p/b888d6d17b5a
本文出自:【張旭童的簡(jiǎn)書(shū)】 (http://www.itdecent.cn/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點(diǎn)個(gè)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。

翻譯:
一個(gè)有序列表(數(shù)據(jù)集)的實(shí)現(xiàn),可以保持ItemData都是有序的,并(自動(dòng))通知列表(RecyclerView)(數(shù)據(jù)集)中的更改。

人話:
首先它是一種數(shù)據(jù)結(jié)構(gòu),是一個(gè)有序的List,list改變后(增刪改查),也可以一直保持?jǐn)?shù)據(jù)集List有序,并且會(huì)自動(dòng)調(diào)用adapter中定向更新notifyXXXX方法,更新RecyclerView。
對(duì)了,它還會(huì)自動(dòng)去重。

關(guān)鍵點(diǎn):
搭配RecyclerView使用,去重,有序,自動(dòng)定向刷新

剛看到這里,我覺(jué)得這特么自動(dòng)定向刷新這一點(diǎn)特性,怎么有點(diǎn)像DiffUtil,后來(lái)我查閱資料才發(fā)現(xiàn),這家伙出來(lái)的比DiffUtil要早,是在Support Library 22 引入的。所以說(shuō)應(yīng)該是DiffUtil像它。

而且SortedList 和 DiffUtil 內(nèi)部 都實(shí)現(xiàn)、持有了一些共同的接口,暴漏出供我們重寫(xiě)比較規(guī)則的Callback的方法名都幾乎一毛一樣。

我個(gè)人感覺(jué)SortedList從設(shè)計(jì)上和DiffUtil比,是有一點(diǎn)點(diǎn)不足,這可能也是官方后來(lái)又在Support Library 24 中引入DiffUtil的一個(gè)理由吧。具體異同,稍后總結(jié)。先看怎么用吧。

Demo效果圖,據(jù)說(shuō)沒(méi)圖的文章沒(méi)人看
Demo效果圖,據(jù)說(shuō)沒(méi)圖的文章沒(méi)人看

用法:

我們來(lái)看看如果使用SortedList該怎么寫(xiě):

Adapter:

要寫(xiě)RecyclerView,就少不了Adapter。
一個(gè)常規(guī)的Adapter內(nèi)部一般持有一個(gè)List<T>的數(shù)據(jù)集,
使用SortedList的話,需要將存儲(chǔ)數(shù)據(jù)源的變量類型改變成SortedList,

  • 唯一差異:將以前的ArrayList->替換為SortedList.

其他的話,倒沒(méi)有變化,因?yàn)镾ortedList雖然沒(méi)有繼承自List,但是暴漏出API還和List一樣的。

public class SortedAdapter extends RecyclerView.Adapter<SortedAdapter.VH> {
    /**
     * 數(shù)據(jù)源替換為SortedList,
     * 以前可能會(huì)用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());
    }
    ...
}

實(shí)體類

無(wú)任何修改,就是一個(gè)普通的實(shí)體類。與上文DiffUtil里的一樣。

Callback:

看過(guò)DiffUtil詳解的同學(xué)對(duì)這個(gè)Callback的編寫(xiě)和理解就易如反掌了,編寫(xiě)規(guī)則和套路和DiffUtil.Callback一樣。
而且還少寫(xiě)一個(gè)方法public Object getChangePayload(int oldItemPosition, int newItemPosition),這里順帶復(fù)習(xí)一下上文內(nèi)容,這個(gè)方法返回 一個(gè) 代表著新老item的改變內(nèi)容的 payload對(duì)象
這里說(shuō)遠(yuǎn)一點(diǎn),關(guān)于這個(gè)少寫(xiě)的方法,正是定向刷新中部分綁定(Partial bind)的核心方法。
DiffUtil是利用這個(gè)getChangePayload()方法的返回值,作為第三個(gè)參數(shù),回調(diào)ListUpdateCallback接口里的void onChanged(int position, int count, Object payload);方法,最終回調(diào)adapter.notifyItemRangeChanged(position, count, payload);方法,再往下就走到Adapter的三參數(shù)onBindViewHolder(VH holder, int position, List<Object> payloads)方法,也就是我們部分綁定所操作的地方了,不太明白的可以去看DiffUtil詳解.
除此之外,耶不用傳新舊數(shù)據(jù)集進(jìn)來(lái)了,里面的每個(gè)方法都是直接傳入ItemData進(jìn)行比較。
那么我們的SortedList的Callback如下編寫(xiě):

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

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

    /**
     * 和DiffUtil方法一致,用來(lái)判斷 兩個(gè)對(duì)象是否是相同的Item。
     */
    @Override
    public boolean areItemsTheSame(TestSortBean item1, TestSortBean item2) {
        return item1.getId() == item2.getId();
    }
    /**
     * 和DiffUtil方法一致,返回false,代表Item內(nèi)容改變。會(huì)回調(diào)mCallback.onChanged()方法;
     */
    @Override
    public boolean areContentsTheSame(TestSortBean oldItem, TestSortBean newItem) {
        //默認(rèn)相同 有一個(gè)不同就是不同
        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的編寫(xiě)也沒(méi)啥大變化,區(qū)別如下:

  • 以前構(gòu)建Adapter時(shí),一般會(huì)將data也一起傳入,現(xiàn)在可傳可不傳。
  • SortedList初始化的時(shí)候,要將Adapter傳進(jìn)來(lái)。所以先構(gòu)建Adapter,再構(gòu)建SortedList
public class SortedListActivity extends AppCompatActivity {
    /**
     * 數(shù)據(jù)源替換為SortedList,
     * 以前可能會(huì)用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));
        //★以前構(gòu)建Adapter時(shí),一般會(huì)將data也一起傳入,現(xiàn)在有變化
        mAdapter = new SortedAdapter(this, null);
        mRv.setAdapter(mAdapter);
        initData();
        mAdapter.setDatas(mDatas);

    }

    private void initData() {
        //★SortedList初始化的時(shí)候,要將Adapter傳進(jìn)來(lái)。所以先構(gòu)建Adapter,再構(gòu)建SortedList
        mDatas = new SortedList<>(TestSortBean.class, new SortedListCallback(mAdapter));
        mDatas.add(new TestSortBean(10, "Android", R.drawable.pic1));
        //★注意這里有一個(gè)重復(fù)的字段 會(huì)自動(dòng)去重的。
        mDatas.add(new TestSortBean(10, "Android重復(fù)", 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, "手撕產(chǎn)品", R.drawable.pic4));
        mDatas.add(new TestSortBean(50, "手撕測(cè)試", R.drawable.pic5));
    }

代碼寫(xiě)到這里,界面就可以正常顯示了。效果如Gif圖。
可以看到雖然我們add進(jìn)去的數(shù)據(jù) 是有重復(fù)的,順序也是亂序的。
但是列表界面依然按照id的升序顯示。
到這就完了嗎,還沒(méi)有說(shuō)到自動(dòng)定向刷新呢。

0步自動(dòng)定向刷新

DiffUtil兩步完成定向刷新比,SortedList這一點(diǎn)真的是很強(qiáng)。0步完成自動(dòng)定向刷新。

新增一條:

在上述代碼的基礎(chǔ)上,如果此時(shí)查詢數(shù)據(jù)庫(kù),發(fā)現(xiàn)有一條新的IM聊天信息,那么直接add()進(jìn)來(lái)即可:
add 內(nèi)部會(huì)自動(dòng)調(diào)用 mCallback.onInserted(index, 1) ->notifyItemRangeInserted(index,1)
也就是說(shuō)我們add一次 它就notify一次,沒(méi)有batch操作,有點(diǎn)low

        mDatas.add(new TestSortBean(26, "溫油對(duì)待產(chǎn)品", R.drawable.pic6));//模擬新增
        mDatas.add(new TestSortBean(12, "小馬可以來(lái)點(diǎn)贊了", R.drawable.pic6));//模擬新增
        mDatas.add(new TestSortBean(2, "Python", R.drawable.pic6));//add進(jìn)去 重復(fù)的會(huì)自動(dòng)修改

新增一坨:

如果是一坨消息,可以用addAll(),查看源碼,它內(nèi)部會(huì)自動(dòng)做Batch操作,beginBatchedUpdates();endBatchedUpdates();。所以如果想batch,就必須用addAll()操作,感覺(jué)這算一個(gè)限制。

        //addAll 也分兩種
        //第一種 以可變參數(shù)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);

刷新

而如果是刷新的場(chǎng)景,可能就不太適用了,刷新時(shí),服務(wù)器給我們的一般都是一個(gè)List,直接addAll 要先clear, 會(huì)閃屏:

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

異步操作

查看源碼,SortedList是在每次add()addAll()、clear().....等對(duì)數(shù)據(jù)集進(jìn)行增刪改查的函數(shù)里,都會(huì)進(jìn)行一遍排序和去重。這排序和去重顯然是個(gè)耗時(shí)操作。那么我想說(shuō)能不能用異步處理呢?丟在子線程中。
于是我如下寫(xiě):

        //每次add都會(huì)計(jì)算一次 想放在子線程中
       new Thread(new Runnable() {
            @Override
            public void run() {
                mDatas.add(new TestSortBean(26, "帥", R.drawable.pic6));//模擬新增數(shù)據(jù)
                mDatas.add(new TestSortBean(27, "帥", R.drawable.pic6));//模擬新增數(shù)據(jù)
            }
        }).start();
    }

然而這是肯定不行的,上文提過(guò),每次add 會(huì)自動(dòng) mAdapter.notifyItemRangeInserted(position, count);
在線程中操作UI,會(huì)android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
這一點(diǎn)就不如DiffUtil啦。

和DiffUtil的異同

它們兩真的很像,而且不管是DiffUtil計(jì)算出Diff后,還是SortedList修改過(guò)數(shù)據(jù)后,內(nèi)部持有的回調(diào)接口都是同一個(gè):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);
}

我就不解析源碼了,非常簡(jiǎn)單,大致流程就是:
DiffUtil計(jì)算出Diff或者SortedList察覺(jué)出數(shù)據(jù)集有改變后,在合適的時(shí)機(jī),回調(diào)ListUpdateCallback接口的這四個(gè)方法,DiffUtilSortedList提供的默認(rèn)Callback實(shí)現(xiàn)中,都會(huì)通知Adapter完成定向刷新。
這就是自動(dòng)定向刷新的原理。

總結(jié)一下它們的異同吧:

  • DiffUtil比較兩個(gè)數(shù)據(jù)源(一般是List)的差異(Diff),Callback中比對(duì)時(shí) 傳遞的參數(shù)是 position
  • SortedList 能完成數(shù)據(jù)集的排序去重, Callback中比對(duì)時(shí),傳遞的是ItemData (JavaBean)。
  • DiffUtil 能完成自動(dòng)定向刷新 + 部分綁定
  • SortedList 只能完成自動(dòng)定向刷新
  • DiffUtil 更通用,SortedList還與數(shù)據(jù)結(jié)構(gòu)耦合
  • DiffUtils: 檢測(cè)不出重復(fù)的,會(huì)被認(rèn)為是新增的。(因?yàn)楸葘?duì)的核心是postion。 所以無(wú)法去重) 但是IM這種消息順序移動(dòng)會(huì)被檢測(cè)到。
  • 它們都是一種自動(dòng)定向刷新的手段

感受總結(jié):

使用SortedList的話,Adapter的保存數(shù)據(jù)集的變量類型要改變。
對(duì)代碼有侵入性,沒(méi)有熱插拔的快感。
在項(xiàng)目中有各種BaseAdapter的前提下,可能要擴(kuò)展一種BaseSortedListAdater更方便使用。

只不過(guò)它的目的不是在定向刷新,而是維護(hù)數(shù)據(jù)集的 有序 & 去重 。
順帶有一個(gè)定向刷新的功能。

而DiffUtil主打的就是 比較集合的差異,更是幫我們自動(dòng)完成定向刷新。

所以SortedList 不適用于 服務(wù)器給所有數(shù)據(jù)過(guò)來(lái)的,下拉刷新情況。此時(shí)不如使用普通的List。

它的亮點(diǎn)和核心,還是在于 有序 & 去重 。

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

但它在特定場(chǎng)景下,例如 數(shù)據(jù)集每次更新時(shí)是增量更新,且需要維持一個(gè)排序規(guī)則的時(shí)候,就像城市列表界面,還是給我們帶來(lái)了一定的便利之處的。

轉(zhuǎn)載請(qǐng)標(biāo)明出處: http://www.itdecent.cn/p/b888d6d17b5a
本文出自:【張旭童的簡(jiǎn)書(shū)】 (http://www.itdecent.cn/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點(diǎn)個(gè)star。多謝
https://github.com/mcxtzhang/SupportDemos

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,716評(píng)論 25 709
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評(píng)論 19 139
  • 1 本來(lái)歪姐不想摻和薛之謙的事,畢竟娛樂(lè)圈的破事兒就像孩子的臉,說(shuō)變就變。 然而他和李雨桐的互撕沒(méi)完沒(méi)了,高潮迭起...
    歪理心說(shuō)閱讀 435評(píng)論 0 1
  • http://mp.weixin.qq.com/s?__biz=MzIzMTcyMzYzNQ==&mid=2247...
    白罐上品官微閱讀 390評(píng)論 0 0
  • 某日凌晨三點(diǎn),小李子在微信上面問(wèn)我在不在,我本想無(wú)視,連續(xù)一周熬夜加班我實(shí)在沒(méi)有精力去搭理他。他直接揭穿,我剛剛看...
    壹分君xi閱讀 4,035評(píng)論 32 69

友情鏈接更多精彩內(nèi)容