你可能不知道的Support(一) 0步自動定向刷新:SortedList

背景:

打算寫一個系列了,講解Android Support包內那些經常使用or冷門有用的工具類的合集。javascript

最近leader在優化IM會話列表,同事之前的作法是無腦notifyDatasetChanged()刷新RecyclerView的。
在消息聊得很嗨不少的時候,界面頻繁刷新,會話列表會出現丟失焦點現象。且性能畢竟不高。
遂想採用定向刷新java

同事知道我之前研究過DiffUtil和定向刷新相關內容,因而便和我討論。android

(不知道DiffUtil的點這裏)blog.csdn.net/zxt0601/art…git

(不瞭解定向刷新的點這裏)blog.csdn.net/zxt0601/art…github

因爲IM會話列表是從數據庫裏讀的,他還告訴我會有數據集重複的現象,且會話列表確定是按時間排序的,因此這對咱們的數據組織提出了兩點要求:有序、去重數據庫

個人想法是:服務器

  • 採用DiffUtil自動計算新老數據集差別,而後自動完成定向刷新
  • 至於數據集的去重和有序,我打算用TreeSet去幫助咱們作。

利用Set自己元素不重複的特性,加之Tree的有序性,來解決數據組織的兩個需求。數據結構

但是leader不知道從哪搜出來一個SortedList,告訴我這是Android SDK提供的。也能夠完成排序and去重。
我心說這是哪路神仙,我覺得是JDK給的呢,因而也查閱了一番資料,遂揭開了SortedList的神祕面紗,也有了今天的文章。app

轉載請標明出處:
gold.xitu.io/post/58416f…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…異步

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效果圖,聽說沒圖的文章沒人看

用法:

咱們來看看若是使用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)

但它在特定場景下,例如 數據集每次更新時是增量更新,且須要維持一個排序規則的時候,就像城市列表界面,仍是給咱們帶來了必定的便利之處的。

轉載請標明出處:
gold.xitu.io/post/58416f…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…

相關文章
相關標籤/搜索