androidx.paging.PageKeyedDataSource Maven / Gradle / Ivy
/*
* Copyright 2016-2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.paging;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Incremental data loader for page-keyed content, where requests return keys for next/previous
* pages.
*
* Implement a DataSource using PageKeyedDataSource if you need to use data from page {@code N - 1}
* to load page {@code N}. This is common, for example, in network APIs that include a next/previous
* link or key with each page load.
*
* The {@code InMemoryByPageRepository} in the
* PagingWithNetworkSample
* shows how to implement a network PageKeyedDataSource using
* Retrofit, while
* handling swipe-to-refresh, network errors, and retry.
*
* @param Type of data used to query Value types out of the DataSource.
* @param Type of items being loaded by the DataSource.
*/
public abstract class PageKeyedDataSource extends ContiguousDataSource {
private final Object mKeyLock = new Object();
@Nullable
private Key mNextKey = null;
@Nullable
private Key mPreviousKey = null;
private void initKeys(@Nullable Key previousKey, @Nullable Key nextKey) {
synchronized (mKeyLock) {
mPreviousKey = previousKey;
mNextKey = nextKey;
}
}
private void setPreviousKey(@Nullable Key previousKey) {
synchronized (mKeyLock) {
mPreviousKey = previousKey;
}
}
private void setNextKey(@Nullable Key nextKey) {
synchronized (mKeyLock) {
mNextKey = nextKey;
}
}
private @Nullable Key getPreviousKey() {
synchronized (mKeyLock) {
return mPreviousKey;
}
}
private @Nullable Key getNextKey() {
synchronized (mKeyLock) {
return mNextKey;
}
}
/**
* Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
*
* @param Type of data used to query pages.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadInitialParams {
/**
* Requested number of items to load.
*
* Note that this may be larger than available data.
*/
public final int requestedLoadSize;
/**
* Defines whether placeholders are enabled, and whether the total count passed to
* {@link LoadInitialCallback#onResult(List, int, int, Key, Key)} will be ignored.
*/
public final boolean placeholdersEnabled;
public LoadInitialParams(int requestedLoadSize, boolean placeholdersEnabled) {
this.requestedLoadSize = requestedLoadSize;
this.placeholdersEnabled = placeholdersEnabled;
}
}
/**
* Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} and
* {@link #loadAfter(LoadParams, LoadCallback)}.
*
* @param Type of data used to query pages.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadParams {
/**
* Load items before/after this key.
*
* Returned data must begin directly adjacent to this position.
*/
public final Key key;
/**
* Requested number of items to load.
*
* Returned page can be of this size, but it may be altered if that is easier, e.g. a
* network data source where the backend defines page size.
*/
public final int requestedLoadSize;
public LoadParams(Key key, int requestedLoadSize) {
this.key = key;
this.requestedLoadSize = requestedLoadSize;
}
}
/**
* Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
* to return data and, optionally, position/count information.
*
* A callback can be called only once, and will throw if called again.
*
* If you can compute the number of items in the data set before and after the loaded range,
* call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that
* information. You can skip passing this information by calling the three parameter
* {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if
* {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning
* information will be ignored.
*
* It is always valid for a DataSource loading method that takes a callback to stash the
* callback and call it later. This enables DataSources to be fully asynchronous, and to handle
* temporary, recoverable error states (such as a network error that can be retried).
*
* @param Type of data used to query pages.
* @param Type of items being loaded.
*/
public abstract static class LoadInitialCallback {
/**
* Called to pass initial load state from a DataSource.
*
* Call this method from your DataSource's {@code loadInitial} function to return data,
* and inform how many placeholders should be shown before and after. If counting is cheap
* to compute (for example, if a network load returns the information regardless), it's
* recommended to pass data back through this method.
*
* It is always valid to pass a different amount of data than what is requested. Pass an
* empty list if there is no more data to load.
*
* @param data List of items loaded from the DataSource. If this is empty, the DataSource
* is treated as empty, and no further loads will occur.
* @param position Position of the item at the front of the list. If there are {@code N}
* items before the items in data that can be loaded from this DataSource,
* pass {@code N}.
* @param totalCount Total number of items that may be returned from this DataSource.
* Includes the number in the initial {@code data} parameter
* as well as any items that can be loaded in front or behind of
* {@code data}.
*/
public abstract void onResult(@NonNull List data, int position, int totalCount,
@Nullable Key previousPageKey, @Nullable Key nextPageKey);
/**
* Called to pass loaded data from a DataSource.
*
* Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
* initialize without counting available data, or supporting placeholders.
*
* It is always valid to pass a different amount of data than what is requested. Pass an
* empty list if there is no more data to load.
*
* @param data List of items loaded from the PageKeyedDataSource.
* @param previousPageKey Key for page before the initial load result, or {@code null} if no
* more data can be loaded before.
* @param nextPageKey Key for page after the initial load result, or {@code null} if no
* more data can be loaded after.
*/
public abstract void onResult(@NonNull List data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey);
}
/**
* Callback for PageKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} and
* {@link #loadAfter(LoadParams, LoadCallback)} to return data.
*
* A callback can be called only once, and will throw if called again.
*
* It is always valid for a DataSource loading method that takes a callback to stash the
* callback and call it later. This enables DataSources to be fully asynchronous, and to handle
* temporary, recoverable error states (such as a network error that can be retried).
*
* @param Type of data used to query pages.
* @param Type of items being loaded.
*/
public abstract static class LoadCallback {
/**
* Called to pass loaded data from a DataSource.
*
* Call this method from your PageKeyedDataSource's
* {@link #loadBefore(LoadParams, LoadCallback)} and
* {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
*
* It is always valid to pass a different amount of data than what is requested. Pass an
* empty list if there is no more data to load.
*
* Pass the key for the subsequent page to load to adjacentPageKey. For example, if you've
* loaded a page in {@link #loadBefore(LoadParams, LoadCallback)}, pass the key for the
* previous page, or {@code null} if the loaded page is the first. If in
* {@link #loadAfter(LoadParams, LoadCallback)}, pass the key for the next page, or
* {@code null} if the loaded page is the last.
*
* @param data List of items loaded from the PageKeyedDataSource.
* @param adjacentPageKey Key for subsequent page load (previous page in {@link #loadBefore}
* / next page in {@link #loadAfter}), or {@code null} if there are
* no more pages to load in the current load direction.
*/
public abstract void onResult(@NonNull List data, @Nullable Key adjacentPageKey);
}
static class LoadInitialCallbackImpl extends LoadInitialCallback {
final LoadCallbackHelper mCallbackHelper;
private final PageKeyedDataSource mDataSource;
private final boolean mCountingEnabled;
LoadInitialCallbackImpl(@NonNull PageKeyedDataSource dataSource,
boolean countingEnabled, @NonNull PageResult.Receiver receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, PageResult.INIT, null, receiver);
mDataSource = dataSource;
mCountingEnabled = countingEnabled;
}
@Override
public void onResult(@NonNull List data, int position, int totalCount,
@Nullable Key previousPageKey, @Nullable Key nextPageKey) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
// setup keys before dispatching data, so guaranteed to be ready
mDataSource.initKeys(previousPageKey, nextPageKey);
int trailingUnloadedCount = totalCount - position - data.size();
if (mCountingEnabled) {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
data, position, trailingUnloadedCount, 0));
} else {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
}
@Override
public void onResult(@NonNull List data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
mDataSource.initKeys(previousPageKey, nextPageKey);
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
}
static class LoadCallbackImpl extends LoadCallback {
final LoadCallbackHelper mCallbackHelper;
private final PageKeyedDataSource mDataSource;
LoadCallbackImpl(@NonNull PageKeyedDataSource dataSource,
@PageResult.ResultType int type, @Nullable Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, type, mainThreadExecutor, receiver);
mDataSource = dataSource;
}
@Override
public void onResult(@NonNull List data, @Nullable Key adjacentPageKey) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
if (mCallbackHelper.mResultType == PageResult.APPEND) {
mDataSource.setNextKey(adjacentPageKey);
} else {
mDataSource.setPreviousKey(adjacentPageKey);
}
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
}
@Nullable
@Override
final Key getKey(int position, Value item) {
// don't attempt to persist keys, since we currently don't pass them to initial load
return null;
}
@Override
final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
LoadInitialCallbackImpl callback =
new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
loadInitial(new LoadInitialParams(initialLoadSize, enablePlaceholders), 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);
}
@Override
final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
@Nullable Key key = getNextKey();
if (key != null) {
loadAfter(new LoadParams<>(key, pageSize),
new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
}
}
@Override
final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
@Nullable Key key = getPreviousKey();
if (key != null) {
loadBefore(new LoadParams<>(key, pageSize),
new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
}
}
/**
* Load initial data.
*
* This method is called first to initialize a PagedList with data. If it's possible to count
* the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
* the callback via the three-parameter
* {@link LoadInitialCallback#onResult(List, int, int, Object, Object)}. This enables PagedLists
* presenting data from this source to display placeholders to represent unloaded items.
*
* {@link LoadInitialParams#requestedLoadSize} is a hint, not a requirement, so it may be may be
* altered or ignored.
*
* @param params Parameters for initial load, including requested load size.
* @param callback Callback that receives initial load data.
*/
public abstract void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback callback);
/**
* Prepend page with the key specified by {@link LoadParams#key LoadParams.key}.
*
* It's valid to return a different list size than the page size if it's easier, e.g. if your
* backend defines page sizes. It is generally safer to increase the number loaded than reduce.
*
* Data may be passed synchronously during the load method, or deferred and called at a
* later time. Further loads going down will be blocked until the callback is called.
*
* If data cannot be loaded (for example, if the request is invalid, or the data would be stale
* and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
* and prevent further loading.
*
* @param params Parameters for the load, including the key for the new page, and requested load
* size.
* @param callback Callback that receives loaded data.
*/
public abstract void loadBefore(@NonNull LoadParams params,
@NonNull LoadCallback callback);
/**
* Append page with the key specified by {@link LoadParams#key LoadParams.key}.
*
* It's valid to return a different list size than the page size if it's easier, e.g. if your
* backend defines page sizes. It is generally safer to increase the number loaded than reduce.
*
* Data may be passed synchronously during the load method, or deferred and called at a
* later time. Further loads going down will be blocked until the callback is called.
*
* If data cannot be loaded (for example, if the request is invalid, or the data would be stale
* and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
* and prevent further loading.
*
* @param params Parameters for the load, including the key for the new page, and requested load
* size.
* @param callback Callback that receives loaded data.
*/
public abstract void loadAfter(@NonNull LoadParams params,
@NonNull LoadCallback callback);
@NonNull
@Override
public final PageKeyedDataSource mapByPage(
@NonNull Function, List> function) {
return new WrapperPageKeyedDataSource<>(this, function);
}
@NonNull
@Override
public final PageKeyedDataSource map(
@NonNull Function function) {
return mapByPage(createListFunction(function));
}
}