Paging流程分析

本篇分析的是paging版本2.1.1的流程
因为jetpack目前更新频率较快,可能过几周这部分代码会有变动

1.分析

上篇的例子中的ViewModel创建方法为

class PersonViewModel : ViewModel {
    private var liveData: LiveData<PagedList<Person>>
    constructor() {
        val factory = PersonFactory()
        val config = PagedList.Config.Builder()
                .setPageSize(pageSize)
                .setInitialLoadSizeHint(pageSize)
                .setEnablePlaceholders(false)
                .setPrefetchDistance(5)
                .build();
        liveData = LivePagedListBuilder<Int, Person>(factory, config).build()
    }

    fun getPersonLiveData(): LiveData<PagedList<Person>> {
        return liveData
    }

    inner class PersonFactory : DataSource.Factory<Int, Person>() {
        override fun create(): DataSource<Int, Person> {
//            return PersonDataSource()
//        return PersonDataPageKeySource()
            return PersonDataItemKeySource()
        }
    }
}

首先会创建一个LiveData<PagedList< Person >>,之后回监听LiveData变化,发生变化再用

 public void submitList(@Nullable PagedList<T> pagedList)

这个方法通知适配器

 private Executor mFetchExecutor = ArchTaskExecutor.getIOThreadExecutor();

 public LiveData<PagedList<Value>> build() {
        return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
                ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
    }

 private static <Key, Value> LiveData<PagedList<Value>> create(
            @Nullable final Key initialLoadKey,
            @NonNull final PagedList.Config config,
            @Nullable final PagedList.BoundaryCallback boundaryCallback,
            @NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
            @NonNull final Executor notifyExecutor,
            @NonNull final Executor fetchExecutor) {
        return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
            @Nullable
            private PagedList<Value> mList;
            @Nullable
            private DataSource<Key, Value> mDataSource;

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

            @SuppressWarnings("unchecked") // for casting getLastKey to Key
            @Override
            protected PagedList<Value> compute() {
                @Nullable Key initializeKey = initialLoadKey;
                if (mList != null) {
                    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;
            }
        }.getLiveData();
    }

可以看出这里会创建一个ComputableLiveData<PagedList< Value >>,然后拿到里面的LiveData对象;这里重写了compute方法,然后拿到传入的DataSourceFactory,从其中的create方法获取到DataSource,并给这个DataSource设置一个InvalidatedCallback
然后会创建一个PagedList,然后会循环判断有效性,如果是有效的那么直接返回;这里的的有效性主要是检查的DataSource的有效性,在PageList的创建过程中会去检查DataSource,如果已经失效了,那么会把当前要创建的DataSource标记为detach,在这里就要重新创建一遍

ComputableLiveData的构造方法中传入了一个执行器fetchExecutor,从初始化方法看应该是会用IO线程处理逻辑

这里的DataSourceFactory就是 我们定义的那个,在里面返回我们自定义的那三种数据操作DataSource
然后看一下ComputableLiveData的构造方法

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

这里用的是上面的那个IO处理的Executor,然后创建了一个LiveData,重写了onActive方法,从之前的LiveData分析中可以知道,这是一个从非活跃切换到活跃状态会执行的方法(首次创建进入也是会执行的),来看下这个Runnable

  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);
                    }
                }
                // check invalid after releasing compute lock to avoid the following scenario.
                // Thread A runs compute()
                // Thread A checks invalid, it is false
                // Main thread sets invalid to true
                // Thread B runs, fails to acquire compute lock and skips
                // Thread A releases compute lock
                // We've left invalid in set state. The check below recovers.
            } while (computed && mInvalid.get());
        }
    };

这里有两个原子操作,一个是保证整个执行期间的原子性,执行完在finally直接重新设置回去,而另一个mInvalid是检查有效性的,如果执行期间正常,那么就不会重新调用compute方法,如果某个时间点出现问题失效了那么就会重新创建DataSource和相应的PageList
目前发现的重新设置为无效的方法在TiledDataSource中,而这个类已经被打上了Deprecated标记,后面也不会讨论这个DataSource,所以暂时可以认为这个失效性判断是没有的, 首次执行完就会变成false,也就是说后续一直有效
回到上面的onActive方法,测试中,每当屏幕从熄屏变为亮屏时,这个方法都会执行,而后续断点到mInvalid这里时,一直都没有执行这里的逻辑,也就是正常情况是只有第一次会执行并发送postValue通知
也就是说我们注册的observer也就只有第一次会通知我们创建了 PagedList< Person > 这个,然后我们把这个和adapter绑定后,后续都不会接收到这个通知了,比如我在之前的例子onChange方法加一个打印信息

E/1234: loadInitial
E/1234: onChanged pageListReceive -> []
E/1234: loadAfter 9
E/1234: loadAfter 19
E/1234: loadAfter 29
E/1234: loadAfter 39
E/1234: loadAfter 49

会发现后面分页都不会再走这个监听,所以这个仅仅是一个绑定的作用

回到第一个方法中,会创建一个PagedList对象,build方法最后会调用到create方法

static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
            @NonNull Executor notifyExecutor,
            @NonNull Executor fetchExecutor,
            @Nullable BoundaryCallback<T> boundaryCallback,
            @NonNull Config config,
            @Nullable K key) {
        if (dataSource.isContiguous() || !config.enablePlaceholders) {
            int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
            if (!dataSource.isContiguous()) {
                //noinspection unchecked
                dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
                        .wrapAsContiguousWithoutPlaceholders();
                if (key != null) {
                    lastLoad = (Integer) key;
                }
            }
            ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
            return new ContiguousPagedList<>(contigDataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    key,
                    lastLoad);
        } else {
            return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    (key != null) ? (Integer) key : 0);
        }
    }

这里会根据情况划分两种PageLiset,有两种条件判断
isContiguous 只有PositionalDataSource是true,另外两个都是true
enablePlaceholders 默认是true,就是允许null占位补齐,而我们的需求一般都是会把这个给禁用掉,也就是设置成false
所以这个方法我们主要看第一个ContiguousPagedList

ContiguousPagedList(
            @NonNull ContiguousDataSource<K, V> dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback<V> boundaryCallback,
            @NonNull Config config,
            @Nullable K key,
            int lastLoad) {
        super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
                boundaryCallback, config);
        mDataSource = dataSource;
        mLastLoad = lastLoad;

        if (mDataSource.isInvalid()) {
            detach();
        } else {
            mDataSource.dispatchLoadInitial(key,
                    mConfig.initialLoadSizeHint,
                    mConfig.pageSize,
                    mConfig.enablePlaceholders,
                    mMainThreadExecutor,
                    mReceiver);
        }
        mShouldTrim = mDataSource.supportsPageDropping()
                && mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
    }

可以看到这里进行了isInvalid检查,现在一般默认都是false的,然后马上调用dispatchLoadInitial方法分发初始化,而这个方法是个抽象方法

  abstract void dispatchLoadInitial(
            @Nullable Key key,
            int initialLoadSize,
            int pageSize,
            boolean enablePlaceholders,
            @NonNull Executor mainThreadExecutor,
            @NonNull PageResult.Receiver<Value> receiver);

也就是交付给子类处理,比如我们查看一下PositionalDataSource内的实现

 final void dispatchLoadInitial(boolean acceptCount,
            int requestedStartPosition, int requestedLoadSize, int pageSize,
            @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
        LoadInitialCallbackImpl<T> callback =
                new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);

        LoadInitialParams params = new LoadInitialParams(
                requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
        loadInitial(params, callback);

        // If initialLoad's callback is not called within the body, we force any following calls
        // to post to the UI thread. This constructor may be run on a background thread, but
        // after constructor, mutation must happen on UI thread.
        callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
    }

这里会把传过来的初始化需求配置用LoadInitialParams包装,然后调用loadInitial方法,而这个方法也是抽象的,也正是我们需要实现的方法

  public abstract void loadInitial(
            @NonNull LoadInitialParams params,
            @NonNull LoadInitialCallback<T> callback);

然后我们数据处理完毕后会调用callback.onResult(…)方法把我们的数据集合传递回去,而这个callback是上面刚创建的LoadInitialCallbackImpl
这个onResult方法经过处理后会到DataSource的dispatchOnCurrentThread方法内

void dispatchOnCurrentThread(@Nullable PageResult<T> result,
                @Nullable Throwable error, boolean retryable) {
            if (result != null) {
                mReceiver.onPageResult(mResultType, result);
            } else {
                mReceiver.onPageError(mResultType, error, retryable);
            }
        }

而这里的mReceiver是在ContiguousPagedList中创建的

 PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
        @AnyThread
        @Override
        public void onPageResult(@PageResult.ResultType int resultType,
                @NonNull PageResult<V> pageResult) {
            if (pageResult.isInvalid()) {
                detach();
                return;
            }

            if (isDetached()) {
                return;
            }

            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) {
                    mLastLoad =
                           pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
                }
            } else {
                boolean trimFromFront = mLastLoad > mStorage.getMiddleOfLoadedRange();
                boolean skipNewPage = mShouldTrim
                        && mStorage.shouldPreTrimNewPage(
                                mConfig.maxSize, mRequiredRemainder, page.size());
                if (resultType == PageResult.APPEND) {
                    if (skipNewPage && !trimFromFront) {
                        mAppendItemsRequested = 0;
                        mLoadStateManager.setState(LoadType.END, LoadState.IDLE, null);
                    } else {
                        mStorage.appendPage(page, ContiguousPagedList.this);
                    }
                } else if (resultType == PageResult.PREPEND) {
                    if (skipNewPage && trimFromFront) {
                        mPrependItemsRequested = 0;
                        mLoadStateManager.setState(LoadType.START, LoadState.IDLE, null);
                    } else {
                        mStorage.prependPage(page, ContiguousPagedList.this);
                    }
                } else {
                    throw new IllegalArgumentException("unexpected resultType " + resultType);
                }
				......
        }

        @Override
        public void onPageError(@PageResult.ResultType int resultType,
                @NonNull Throwable error, boolean retryable) {
            LoadState errorState = retryable ? LoadState.RETRYABLE_ERROR : LoadState.ERROR;

            if (resultType == PageResult.PREPEND) {
                mLoadStateManager.setState(LoadType.START, errorState, error);
            } else if (resultType == PageResult.APPEND) {
                mLoadStateManager.setState(LoadType.END, errorState, error);
            } else {
                // TODO: pass init signal through to *previous* list
                throw new IllegalStateException("TODO");
            }
        }
    };

比如初始化会进入resultType == PageResult.INIT这个判断,然后调用 mStorage.init(…)方法,而这init方法会做一些数据初始化

 void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
            @NonNull Callback callback) {
        init(leadingNulls, page, trailingNulls, positionOffset);
        callback.onInitialized(size());
    }
 private void init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset) {
        mLeadingNullCount = leadingNulls;
        mPages.clear();
        mPages.add(page);
        mTrailingNullCount = trailingNulls;
        mPositionOffset = positionOffset;
        mLoadedCount = page.size();
        mStorageCount = mLoadedCount;
        mPageSize = page.size();
        mNumberPrepended = 0;
        mNumberAppended = 0;
    }

然后会回调callback.onInitialized,通知此时的数据大小

public void onInitialized(int count) {
        notifyInserted(0, count);
        mReplacePagesWithNulls =
                mStorage.getLeadingNullCount() > 0 || mStorage.getTrailingNullCount() > 0;
    }

private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();

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

在这个方法里最终会遍历注册的callbacks去循环通知有数据插入,那么现在就是在于这个callback是什么了

然后回到我们适配器的创建,上篇的例子中

class PaginAdapter : PagedListAdapter<Person, PaginAdapter.MViewHolder> {

   constructor() : super(MDiffCallBack())

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MViewHolder {
           ......
   }

   override fun onBindViewHolder(holder: MViewHolder, position: Int) {
       	......   
   }

   inner class MViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
       ......
    }

    class MDiffCallBack : DiffUtil.ItemCallback<Person>() {
        override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
            return oldItem.name == (newItem.name) && oldItem.positon == newItem.positon;
        }

        override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
            return oldItem.name == newItem.name;
        }
    }

和正常适配器用法基本一致,不同的是适配器的继承改为了PagedListAdapter,而这个PagedListAdapter本身也是继承RecyclerView.Adapter的,然后就是构造方法需要传入一个DiffUtil.ItemCallback,看一下构造方法

 protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
        mDiffer.addPagedListListener(mListener);
        mDiffer.addLoadStateListener(mLoadStateListener);
    }

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

PageListAdapter会创建一个AsyncPagedListDiffer,而后者的构造方法中会把当前的PageListAdapter传进去,并且会创建一个AdapterListUpdateCallback,而这个callBack正是刷新适配器数据的回调

public final class AdapterListUpdateCallback implements ListUpdateCallback {
    @NonNull
    private final RecyclerView.Adapter mAdapter;
    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }
    @Override
    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }
    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }
    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }
    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

然后我们需要打通这些的联系,而这个方法正是submitList

 public void submitList(@Nullable final PagedList<T> pagedList,
            @Nullable final Runnable commitCallback) {
      	......
        if (mPagedList == null && mSnapshot == null) {
            // fast simple first insert
            mPagedList = pagedList;
            mPagedList.addWeakLoadStateListener(mLoadStateListener);
            pagedList.addWeakCallback(null, mPagedListCallback);
            mUpdateCallback.onInserted(0, pagedList.size());
            onCurrentListChanged(null, pagedList, commitCallback);
            return;
        }
   		  ......
    }

submitList方法进过几轮调用会走到上面的方法中,而pageList.addWeakCallBack正是添加callback数据的那个方法,而且这里会马上调用一次UpdateCallback.onInserted方法去刷新适配器,因为绑定adater和pagelist已经首次数据请求并不是顺序执行的;绑定适配器关系由用户控制,可能无限期滞后,所以首次绑定可能造成已经有数据,但是页面没有刷新的情况,所以这里强制检查刷新一遍

  public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) {
        if (previousSnapshot != null && previousSnapshot != this) {
			......
	       mCallbacks.add(new WeakReference<>(callback));
    }

然后注意一下上面的mPagedListCallback,同步回调了mUpdateCallback的方法,也就是刷新适配器的方法

 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);
        }
    };

这样整个初始化数据流程就走完了
然后是加载更多的逻辑,与普通的用法不同,适配器在bind方法中需要通过getItem方法获取数据对象

 override fun onBindViewHolder(holder: MViewHolder, position: Int) {
            val person = getItem(position)
            person?.apply {
                holder.dataBinding?.person = person
                holder.dataBinding?.executePendingBindings()
            }
        }

那么切入点就在这

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);
    }

getItem方法会进入到pagelist得到loadAround方法中,然后会走到子类ContiguousPagedList中,这里是以我们的例子走的流程,可能有其他分支

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

        mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
        if (mPrependItemsRequested > 0 && mLoadStateManager.getStart() == LoadState.IDLE) {
            schedulePrepend();
        }

        mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
        if (mAppendItemsRequested > 0 && mLoadStateManager.getEnd() == LoadState.IDLE) {
            scheduleAppend();
        }
    }
  
  static int getAppendItemsRequested(
            int prefetchDistance, int index, int itemsBeforeTrailingNulls) {
        return index + prefetchDistance + 1 - itemsBeforeTrailingNulls;
    }

这里会计算出从什么位置开始去加载下一页
就是 当前位置index + 配置的加载偏移量 + 1 > 当前数据的总长度 即是下一页加载的条件
也就是当滑动到最大数量 - 1 - 偏移量时候开始加载下一页,比如有10条数据,匹配的偏移量是5,当getITtem获取到索引是4的数据时候就会去加载下一页

 private void scheduleAppend() {
        mLoadStateManager.setState(LoadType.END, LoadState.LOADING, null);

        final int position = mStorage.getLeadingNullCount()
                + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();

        // safe to access first item here - mStorage can't be empty if we're appending
        final V item = mStorage.getLastLoadedItem();
        mBackgroundThreadExecutor.execute(new Runnable() {
            @Override
            public void run() {
                if (isDetached()) {
                    return;
                }
                if (mDataSource.isInvalid()) {
                    detach();
                } else {
                    mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
                            mMainThreadExecutor, mReceiver);
                }
            }
        });
    }

scheduleAppend方法会把当前的End标记修改为LOADING,加载期间再怎么滑到底部也不会执行
dispatchLoadAfter和上面init方法类似,最后都交付我们自己处理,再通过Result传给我们,然后在通知的方法中

   @MainThread
    @Override
    public void onEmptyAppend() {
        mLoadStateManager.setState(LoadType.END, LoadState.DONE, null);
    }

    @MainThread
    @Override
    public void onPageAppended(int endPosition, int changedCount, int addedCount) {
        // consider whether to post more work, now that a page is fully appended
        mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
        if (mAppendItemsRequested > 0) {
            // not done appending, keep going
            scheduleAppend();
        } else {
            mLoadStateManager.setState(LoadType.END, LoadState.IDLE, null);
        }

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

下一页加载完成,如果发现下下一页的条件还是满足的,那么会继续加载下下一页,否则会把当前End标记修改为IDLE
如果没有数据Empty就会把End修改为DONE,后续也就不再执行

至此,整个Paging的流程就分析完毕了,流程图大致为
在这里插入图片描述

2.总结

1.LivePagedListBuilder的build方法会马上返回LiveData<PagedList< T >>一个对象,但内部的PageList并没有创建,需要等待页面活跃后才会创建
2.PageList创建后会马上通过dispatchLoadInitial方法去通知执行初始化数据方法,这里是交付给我们自定义的DataSources的loadInitial方法处理
3.处理完数据后通过callback.result方法传递数据信息,这个会经过一层层回调最终到PagedList.Callback中,去执行适配器的刷新方法
4.加载更多的判断是在getItem方法中,检测到满足条件的索引条件后会去执行加载下一页,修改当前的State属性,并把加载下一页的逻辑交付我们自定义的DataSource的loadAfter方法中;然后加载数据处理完毕后,再通过层层回调最后通知刷新适配器,并把State属性修改为原本属性或者设置成完成状态
5.首次PageList和适配器绑定会强制刷新一遍全部数据
6.PageList的监听方法只会回调一次,也就是绑定适配器也只是只有一遍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值