1. 程式人生 > >Android Paging分頁元件的使用及原理

Android Paging分頁元件的使用及原理

640?wx_fmt=png

今日科技快訊

近日,人工智慧創業公司Vocalize.ai的實驗室進行了一項語音識別測試,研究人員對虛擬助手Alexa、谷歌助手和Siri進行了測試對比。研究人員分別使用美國本土口音、印度口音和中國口音的英語對三種語音助手進行了測試。三種語音助手對於獨立的單詞識別都完成的很好,但是谷歌助手在理解中國口音的英語方面完全超過了其它兩種語音助手。

作者簡介

週一早上好,新的一週也要繼續加油哦~

本篇轉自 anko 的文章,分享了關於 Android元件Paging的使用及原理,一起來看看!希望大家喜歡。

anko 的部落格地址:

https://ankko.github.io/

前言

首先我們先看一下Paging元件使用的流程

  • 建立DataSource

  • 建立Factory

  • 建立Adapter相關類

  • 建立LivePagedListBuilder

  • 監聽LiveData

640?wx_fmt=gif

正文

  • DataSource

處理資料來源相關抽象類,DataSource<Key, Value>Key是用來幫助開發者進行資料的組合以及請求的變化,會在請求開始和過程中傳遞給開發者,Key的型別和值由開發者決定。Value就是資料來源的實體類。有三個預設提供的類

PageKeyedDataSource<Key, Value>

如果頁面在載入時插入一個/下一個鍵,例如:從網路獲取社交媒體的帖子,可能需要將nextPage載入到後續的載入中

ItemKeyedDataSource<Key, Value>

在需要讓使用的資料的item從N條增加到N+1條時使用,一般的請求用這個類可以大部分解決,KEY值傳page頁數即可

PositionalDataSource

如果需要從資料儲存的任意位置來獲取資料頁面。此類支援你從任何位置開始請求一組item的資料集。例如,該請求可能會返回從位置1200條開始的20個數據項,適合用於本地資料來源的載入

  • PageList

DataSource獲取不可變數量的資料,可以通過Config進行各種配置,將資料提交給Adapter進行展示

PageList.Config
對資料如何處理的配置,控制載入多少資料,什麼時候載入
PagedList.Config.Builder()
                    .setEnablePlaceholders(false
)
                    .setInitialLoadSizeHint(20)
                    .setPageSize(1).build()
  • int pageSize:每個頁面需要載入多少數量

  • int prefetchDistance:滑動剩餘多少item時,去載入下一頁的資料,預設是pageSize大小

  • boolean enablePlaceholders:開啟佔位符

  • int initialLoadSizeHint:第一次載入多少資料量

Placeholders

佔位列表,在你的列表未載入前,是否顯示佔位列表,就像各類新聞app中,沒載入之前,列表顯示的都是一些預設的灰色的佈局,預設開啟

PS:暫時未研究,一般設定為false

優點:

  • 支援滾動條

  • 不需要loading提示,因為List大小是確定的

缺點:

  • 必須確定List的大小

  • 需要每個item大小一致

  • 需要Adapter觸發未載入資料的載入

PagedListAdapter(DiffUtil.ItemCallback diffCallback)

RecyclerView.Adapter的一個實現類,用於當資料載入完畢時,通知 RecyclerView資料已經到達。 RecyclerView就可以把資料填充進來,取代原來的佔位元素。資料變化時,PageListAdapter會接受到通知,交由委託類AsyncPagedListDiffer來處理,AsyncPagedListDiffer是對DiffUtil.ItemCallback<T>持有物件的委託類,AsyncPagedListDiffer使用後臺執行緒來計算PagedList的改變,item是否改變,由DiffUtil.ItemCallback<T>決定。

DataSource.Factory<Key, Value>

如何建立DataSource的Factory類,主要工作是建立DataSource

LivePagedListBuilder

根據提供的Factory和PageConfig來建立資料來源,返回資料為LiveData<PagedList<Value>>

val pagedList: LiveData<PagedList<Entity>> = LivePagedListBuilder(sourceFactory, pagedListConfig).build()

例子

DataSource

class ListDataSource : ItemKeyedDataSource<Int,Entity>() {

var page: Int = 1

    override fun loadInitial(params: LoadInitialParams<Int>, callback:  LoadInitialCallback<Entity>) {
        //初始請求資料,必須要同步請求
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Entity>) {

        //請求後續資料,非同步
         page++

    }
}
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<V>) {

    }

    override fun getKey(item: Entity)Int {
        return page
    }

Factory

class ListFactory : DataSource.Factory<Int, Entity>() {

     var sourceLiveData = MutableLiveData<ListDataSource>()

    override fun create(): DataSource<Int, Entity> {
        val dataSource = ListDataSource()
        sourceLiveData.postValue(dataSource)
        return dataSource
    }
}

Adapter

class ListAdapter() :
        PagedListAdapter<Entity, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<Entity>() {
            override fun areItemsTheSame(oldItem: Entity, newItem: Entity)Boolean =
                    oldItem.name == newItem.name

            override fun areContentsTheSame(oldItem: Entity, newItem: Entity)Boolean =
                    oldItem == newItem
        }) {


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = ListViewHolder.create(parent)

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as ListViewHolder).bind(getItem(position))
    }
}

ViewHolder

class ListViewHolder(parent: View) : RecyclerView.ViewHolder(parent) {
    private val contentTv = itemView.findViewById<TextView>(R.id.contentTv)
    fun bind(entity: WeiboMessageEntity?) {
        contentTv.name = entity?.name
    }

    companion object {
        fun create(parent: ViewGroup): ListViewHolder {
            val view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.item, parent, false)
            return ListViewHolder(view)
        }
    }
}

LivePagedListBuilder

val sourceFactory = ListFactory()
val pagedListConfig = PagedList.Config.Builder()
                .setEnablePlaceholders(false)
                .setInitialLoadSizeHint(20*2)
                .setPageSize(20)
                .build()
val pagedList: LiveData<PagedList<Entity>> = LivePagedListBuilder(sourceFactory, pagedListConfig).build()

UI

viewModel.pagedList.observe(this, Observer {
            listAdapter?.submitList(it)
        })

原理

從使用角度分析,從LivePagedListBuilder開始

LivePagedListBuilder-build(根據Factory和DataSource來構建包含資料來源LiveData的PageList)。

建立 ComputableLiveData(建立的時候就會執行mRefreshRunnable),建立後返回LiveData。

private static <Key, Value> LiveData<PagedList<Value>> create(...) {
    return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
        ...

        @Override
        protected PagedList<Value> compute() {
            ...
            return mList;
        }
    }.getLiveData();

執行Runnable,mRefreshRunnable執行緒完成計算工作後,呼叫mLiveData.postValue(value), View層的Observer則會接收到結果。

public ComputableLiveData(@NonNull Executor executor) {
        mExecutor = executor;
        mLiveData = new LiveData<T>() {
            @Override
            protected void onActive() {
                mExecutor.execute(mRefreshRunnable);
            }
        };
    }

 @VisibleForTesting
    final Runnable mRefreshRunnable = new Runnable() {
        @WorkerThread
        @Override
        public void run() {
            boolean computed;
            do {
                computed = false;
                // compute can happen only in 1 thread but no reason to lock others.
                if (mComputing.compareAndSet(falsetrue)) {
                    // as long as it is invalid, keep computing.
                    try {
                        T value = null;
                        while (mInvalid.compareAndSet(truefalse)) {
                            computed = true;
                            value = compute();
                        }
                        if (computed) {
                            mLiveData.postValue(value);
                        }
                    } finally {
                        // release compute lock
                        mComputing.set(false);
                    }
                }
            } while (computed && mInvalid.get());
        }
    };

computed() 計算工作主要是建立PageList,只會計算一次,計算完後會返回List

protected PagedList<Value> compute() {
    @Nullable Key initializeKey = initialLoadKey;
    if (mList != null) {
        //noinspection unchecked
        initializeKey = (Key) mList.getLastKey();
    }

    do {
        if (mDataSource != null) {
            mDataSource.removeInvalidatedCallback(mCallback);
        }

        mDataSource = dataSourceFactory.create();
        mDataSource.addInvalidatedCallback(mCallback);

        mList = new PagedList.Builder<>(mDataSource, config)
                .setNotifyExecutor(notifyExecutor)
                .setFetchExecutor(fetchExecutor)
                .setBoundaryCallback(boundaryCallback)
                .setInitialKey(initializeKey)
                .build();
    } while (mList.isDetached());
    return mList;
}

一系列的建立以及呼叫工作

  • PageList-build-create

  • 建立 ContiguousPagedList 或 TiledPagedList(if isContiguous is true) 如果保證資料的item數不會變化,則可以設定這個屬性

  • 呼叫 dispatchLoadInitial

  • 建立 LoadInitialCallbackImpl

  • 呼叫我們需要編寫程式碼的 loadInitial(如果此時載入資料失敗可以呼叫loadInitial()重新進行請求)

  • 呼叫 callBack.onResult() 返回資料

  • 回撥至 LoadInitialCallbackImpl

根據原資料重新建立PageList,呼叫dispatchResultToReceiver

void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
    Executor executor;
    ...

    if (executor != null) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                mReceiver.onPageResult(mResultType, result);
            }
        });
    } else {
        mReceiver.onPageResult(mResultType, result);
    }
}

注意這裡的executor,這裡就是為什麼我們需要在loadInitial使用同步請求的原因。當我們呼叫重新整理方法後,會重新執行一開始初始化的mRefreshRunnable

private final DataSource.InvalidatedCallback mCallback =
    new DataSource.InvalidatedCallback() {
    @Override
    public void onInvalidated() {
    invalidate();
    }
    };

 public void invalidate() {
        ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
    }

final Runnable mInvalidationRunnable = new Runnable() {
        @MainThread
        @Override
        public void run() {
            boolean isActive = mLiveData.hasActiveObservers();
            if (mInvalid.compareAndSet(falsetrue)) {
                if (isActive) {
                    mExecutor.execute(mRefreshRunnable);
                }
            }
        }
    };

在compute的計算方法中,會將pageList重新例項化,會優先呼叫loadInitial方法進行初始化,例項化後,將結果返回給compute(),後續在dispatchLoadInitial方法中會進行postExecutor的設定,如果loadInitial方法是非同步的,postExecutor就會優先設定。如果不進行同步操作,會導致資料無法顯示或者時重新整理操作時提前清空了資料,導致顯示不正常。

回到上一步,呼叫dispatchResultToReceiver後會執行 mReceiver.onPageResult, mReceiver就是之前建立的ContiguousPagedList或TiledPagedList。onPageResult方法中根據PageResult的不同狀態處理不同情況

private PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
        public void onPageResult(@PageResult.ResultType int resultType,
                @NonNull PageResult<V> pageResult) {
            ...

            List<V> page = pageResult.page;
            if (resultType == PageResult.INIT) {
                mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
                        pageResult.positionOffset, ContiguousPagedList.this);
                if (mLastLoad == LAST_LOAD_UNSPECIFIED) {
                    // Because the ContiguousPagedList wasn't initialized with a last load position,
                    // initialize it to the middle of the initial load
                    mLastLoad =
                            pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
                }
            } else if (resultType == PageResult.APPEND) {
                mStorage.appendPage(page, ContiguousPagedList.this);
            } else if (resultType == PageResult.PREPEND) {
                mStorage.prependPage(page, ContiguousPagedList.this);
            } else {
                throw new IllegalArgumentException("unexpected resultType " + resultType);
            }
        }
    };

void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
            @NonNull Callback callback) {
        init(leadingNulls, page, trailingNulls, positionOffset);
        callback.onInitialized(size());
    }

第一次顯示列表狀態為 PageResult.INIT,後續載入資料的狀態為PageResult.APPEND,進行一些回撥工作(onChanged,onInserted,onRemoved等)

由於在loadInitial方法中,我們的請求時同步的,所以會在資料處理結束後,View層的LiveData才會接受到資料,接受到資料後呼叫adapter.submitList(it)。

列表初始顯示、滑動或者notifyDataSetChanged時,會呼叫Adapter的getItem,然後委託給AsyncPagedListDiffer的getItem。

public T getItem(int index{
        if (mPagedList == null) {
            if (mSnapshot == null) {
                throw new IndexOutOfBoundsException(
                        "Item count is zero, getItem() call is invalid");
            } else {
                return mSnapshot.get(index);
            }
        }

        mPagedList.loadAround(index);
        return mPagedList.get(index);
    }


    public void loadAround(int index{
        mLastLoad = index + getPositionOffset();
        loadAroundInternal(index);
        ...
    }

    protected void loadAroundInternal(int index{
        int prependItems = mConfig.prefetchDistance - (index - mStorage.getLeadingNullCount());
        int appendItems = index + mConfig.prefetchDistance
                - (mStorage.getLeadingNullCount() + mStorage.getStorageCount());

        mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
        if (mPrependItemsRequested > 0) {
            schedulePrepend();
        }

        mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
        if (mAppendItemsRequested > 0) {
            scheduleAppend();
        }
    }

mPagedList.loadAround(index)-loadAroundInternal(這裡根據設定的prefetchDistance設定載入到多少item時去載入新資料)。schedulePrepend和scheduleAppend是分別呼叫before和after的兩個方法。loadBeforeloadAfter的callBack呼叫在onPageResult方法中不再呼叫mStorage.init而是mStorage.appendPage。

void appendPage(@NonNull List<T> page, @NonNull Callback callback{
        final int count = page.size();
        ...
        callback.onPageAppended(mLeadingNullCount + mStorageCount - count,
                changedCount, addedCount);
    }

public void onPageAppended(int endPosition, int changedCount, int addedCount{
       ...

        // finally dispatch callbacks, after append may have already been scheduled
        notifyChanged(endPosition, changedCount);
        notifyInserted(endPosition + changedCount, addedCount);
    }

void notifyInserted(int position, int count{
        if (count != 0) {
            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
                Callback callback = mCallbacks.get(i).get();
                if (callback != null) {
                    callback.onInserted(position, count);
                }
            }
        }
    }

void notifyChanged(int position, int count{
        if (count != 0) {
            for (int i = mCallba