Android组件Paging的使用及原理

流程

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

paging

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
  • 对数据如何处理的配置,控制加载多少数据,什么时候加载

1
2
3
4
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>>

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

BoundaryCallback

  • 经常用于与ROOM结合使用时

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
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:

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

原理

从使用角度分析,从LivePagedListBuilder开始

  1. LivePagedListBuilder-build(根据Factory和DataSource来构建包含数据源LiveData的PageList)
  2. 创建 ComputableLiveData(创建的时候就会执行mRefreshRunnable),创建后返回LiveData
1
2
3
4
5
6
7
8
9
10
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则会接收到结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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());
    }
    };
  2. computed() 计算工作主要是创建PageList,只会计算一次,计算完后会返回List

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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;
    }
  3. 一系列的创建以及调用工作

  • PageList-build-create
  • 创建 ContiguousPagedList 或 TiledPagedList(if isContiguous is true) 如果保证数据的item数不会变化,则可以设置这个属性
  • 调用 dispatchLoadInitial
  • 创建 LoadInitialCallbackImpl
  • 调用我们需要编写代码的 loadInitial(如果此时加载数据失败可以调用loadInitial()重新进行请求)
  • 调用 callBack.onResult() 返回数据
  • 回调至 LoadInitialCallbackImpl
  1. 根据原数据重新创建PageList,调用dispatchResultToReceiver
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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就是之前创建的ContiguousPagedListTiledPagedList
    onPageResult方法中根据PageResult的不同状态处理不同情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

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

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