美文网首页
Android组件之Paging的使用及原理

Android组件之Paging的使用及原理

作者: 安安_660c | 来源:发表于2022-10-20 17:19 被阅读0次

    流程

    ● 创建DataSource
    ● 创建Factory
    ● 创建Adapter相关类
    ● 创建LivePagedListBuilder
    ● 监听LiveData

    DataSource

    处理数据源相关抽象类,DataSource<Key, Value>Key是用来帮助开发者进行数据的组合以及请求的变化,会在请求开始和过程中传递给开发者,Key的类型和值由开发者决定
    Value就是数据源的实体类

    有三个默认提供的类:

    ● PageKeyedDataSource<Key, Value>:
    如果页面在加载时插入一个/下一个键,例如:从网络获取社交媒体的帖子,可能需要将nextPage加载到后续的加载中
    ● ItemKeyedDataSource<Key, Value>:
    在需要让使用的数据的item从N条增加到N+1条时使用,一般的请求用这个类可以大部分解决,KEY值传page页数即可
    ● PositionalDataSource<T>:
    如果需要从数据存储的任意位置来获取数据页面。此类支持你从任何位置开始请求一组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中,没加载之前,列表显示的都是一些默认的灰色的布局,默认开启

    优点:
    ● 支持滚动条
    ● 不需要loading提示,因为List大小是确定的

    缺点:
    ● 必须确定List的大小
    ● 需要每个item大小一致
    ● 需要Adapter触发未加载数据的加载

    PagedListAdapter(DiffUtil.ItemCallback<T> 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()
    

    BoundaryCallback

    经常用于与ROOM结合使用时

    public abstract static class BoundaryCallback<T> {
            //如果本地数据获取到的数量为 0 时调用
            public void onZeroItemsLoaded() {}
            //一般不做处理
            public void onItemAtFrontLoaded(@NonNull T itemAtFront) {}
            // 最后一个item加载时调用
            // 假设ROOM中存了1000条数据,但并不是这1000全部拿出来,而是根据你设置的pagedListConfig来加载数据
            //当滑动的时候 根据pagedListConfig的规则,最后一个item需要被加载时,才会调用该方法
            public void onItemAtEndLoaded(@NonNull T itemAtEnd) {}
        }
    

    例子

    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开始

    1. LivePagedListBuilder-build(根据Factory和DataSource来构建包含数据源LiveData的PageList)
    2. 创建 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();
    
    1. 执行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(false, true)) {
                        // as long as it is invalid, keep computing.
                        try {
                            T value = null;
                            while (mInvalid.compareAndSet(true, false)) {
                                computed = true;
                                value = compute();
                            }
                            if (computed) {
                                mLiveData.postValue(value);
                            }
                        } finally {
                            // release compute lock
                            mComputing.set(false);
                        }
                    }
                } while (computed && mInvalid.get());
            }
        };
    
    1. 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;
                }
    
    1. 一系列的创建以及调用工作

    ● PageList-build-create
    ● 创建 ContiguousPagedList 或 TiledPagedList(if isContiguous is true) 如果保证数据的item数不会变化,则可以设置这个属性
    ● 调用 dispatchLoadInitial
    ● 创建 LoadInitialCallbackImpl
    ● 调用我们需要编写代码的 loadInitial(如果此时加载数据失败可以调用loadInitial()重新进行请求)
    ● 调用 callBack.onResult() 返回数据
    ● 回调至 LoadInitialCallbackImpl

    1. 根据原数据重新创建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(false, true)) {
                    if (isActive) {
                        mExecutor.execute(mRefreshRunnable);
                    }
                }
            }
        };
    

    在compute的计算方法中,会将pageList重新实例化,会优先调用loadInitial方法进行初始化,实例化后,将结果返回给compute(),
    后续在dispatchLoadInitial方法中会进行postExecutor的设置,如果loadInitial方法是异步的,postExecutor就会优先设置

    如果不进行同步操作,会导致数据无法显示或者时刷新操作时提前清空了数据,导致显示不正常

    1. 回到上一步,调用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等)

    1. 由于在loadInitial方法中,我们的请求时同步的,所以会在数据处理结束后,View层的LiveData才会接受到数据,接受到数据后调用adapter.submitList(it)
    2. 列表初始显示、滑动或者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的两个方法

    loadBefore和loadAfter的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 = mCallbacks.size() - 1; i >= 0; i--) {
                    Callback callback = mCallbacks.get(i).get();
    
                    if (callback != null) {
                        callback.onChanged(position, count);
                    }
                }
            }
        }
    

    回调至AsyncPagedListDiffer的PagedList.Callback

    private PagedList.Callback mPagedListCallback = new PagedList.Callback() {
            @Override
            public void onInserted(int position, int count) {
                mUpdateCallback.onInserted(position, count);
            }
    
            @Override
            public void onRemoved(int position, int count) {
                mUpdateCallback.onRemoved(position, count);
            }
    
            @Override
            public void onChanged(int position, int count) {
                // NOTE: pass a null payload to convey null -> item
                mUpdateCallback.onChanged(position, count, null);
            }
        };
    

    则是和Adapter绑定的callBack

    public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
                @NonNull DiffUtil.ItemCallback<T> diffCallback) {
            mUpdateCallback = new AdapterListUpdateCallback(adapter);
            mConfig = new AsyncDifferConfig.Builder<T>(diffCallback).build();
        }
    

    这样recyclerView就能接受到我们的数据变化

    转载于:https://www.yuque.com/mikaelzero/blog/epr7ut

    相关文章

      网友评论

          本文标题:Android组件之Paging的使用及原理

          本文链接:https://www.haomeiwen.com/subject/ademzrtx.html