android.arch.paging.PositionalDataSource 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 android.arch.paging;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.arch.core.util.Function;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
* arbitrary page positions.
*
* Extend PositionalDataSource if you can load pages of a requested size at arbitrary
* positions, and provide a fixed item count. If your data source can't support loading arbitrary
* requested page sizes (e.g. when network page size constraints are only known at runtime), use
* either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
*
* Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
* PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
* at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
* If placeholders are disabled, initialize with the two parameter
* {@link LoadInitialCallback#onResult(List, int)}.
*
* Room can generate a Factory of PositionalDataSources for you:
*
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
* public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
* }
*
* @param Type of items being loaded by the PositionalDataSource.
*/
public abstract class PositionalDataSource extends DataSource {
/**
* Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadInitialParams {
/**
* Initial load position requested.
*
* Note that this may not be within the bounds of your data set, it may need to be adjusted
* before you execute your load.
*/
public final int requestedStartPosition;
/**
* Requested number of items to load.
*
* Note that this may be larger than available data.
*/
public final int requestedLoadSize;
/**
* Defines page size acceptable for return values.
*
* List of items passed to the callback must be an integer multiple of page size.
*/
public final int pageSize;
/**
* Defines whether placeholders are enabled, and whether the total count passed to
* {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
*/
public final boolean placeholdersEnabled;
public LoadInitialParams(
int requestedStartPosition,
int requestedLoadSize,
int pageSize,
boolean placeholdersEnabled) {
this.requestedStartPosition = requestedStartPosition;
this.requestedLoadSize = requestedLoadSize;
this.pageSize = pageSize;
this.placeholdersEnabled = placeholdersEnabled;
}
}
/**
* Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadRangeParams {
/**
* Start position of data to load.
*
* Returned data must start at this position.
*/
public final int startPosition;
/**
* Number of items to load.
*
* Returned data must be of this size, unless at end of the list.
*/
public final int loadSize;
public LoadRangeParams(int startPosition, int loadSize) {
this.startPosition = startPosition;
this.loadSize = loadSize;
}
}
/**
* Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
* to return data, position, and count.
*
* A callback should be called only once, and may 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 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 the total size to the totalCount parameter. If placeholders are not
* requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
* call {@link #onResult(List, int)}.
*
* @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);
/**
* Called to pass initial load state from a DataSource without total count,
* when placeholders aren't requested.
* Note: This method can only be called when placeholders
* are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
*
* Call this method from your DataSource's {@code loadInitial} function to return data,
* if position is known but total size is not. If placeholders are requested, call the three
* parameter variant: {@link #onResult(List, int, int)}.
*
* @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 provided by this DataSource,
* pass {@code N}.
*/
public abstract void onResult(@NonNull List data, int position);
}
/**
* Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
* to return data.
*
* A callback should be called only once, and may 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 items being loaded.
*/
public abstract static class LoadRangeCallback {
/**
* Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
*
* @param data List of items loaded from the DataSource. Must be same size as requested,
* unless at end of list.
*/
public abstract void onResult(@NonNull List data);
}
static class LoadInitialCallbackImpl extends LoadInitialCallback {
final LoadCallbackHelper mCallbackHelper;
private final boolean mCountingEnabled;
private final int mPageSize;
LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
int pageSize, PageResult.Receiver receiver) {
mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
mCountingEnabled = countingEnabled;
mPageSize = pageSize;
if (mPageSize < 1) {
throw new IllegalArgumentException("Page size must be non-negative");
}
}
@Override
public void onResult(@NonNull List data, int position, int totalCount) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
if (position + data.size() != totalCount
&& data.size() % mPageSize != 0) {
throw new IllegalArgumentException("PositionalDataSource requires initial load"
+ " size to be a multiple of page size to support internal tiling."
+ " loadSize " + data.size() + ", position " + position
+ ", totalCount " + totalCount + ", pageSize " + mPageSize);
}
if (mCountingEnabled) {
int trailingUnloadedCount = totalCount - position - data.size();
mCallbackHelper.dispatchResultToReceiver(
new PageResult<>(data, position, trailingUnloadedCount, 0));
} else {
// Only occurs when wrapped as contiguous
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
}
@Override
public void onResult(@NonNull List data, int position) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
if (position < 0) {
throw new IllegalArgumentException("Position must be non-negative");
}
if (data.isEmpty() && position != 0) {
throw new IllegalArgumentException(
"Initial result cannot be empty if items are present in data set.");
}
if (mCountingEnabled) {
throw new IllegalStateException("Placeholders requested, but totalCount not"
+ " provided. Please call the three-parameter onResult method, or"
+ " disable placeholders in the PagedList.Config");
}
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
}
static class LoadRangeCallbackImpl extends LoadRangeCallback {
private LoadCallbackHelper mCallbackHelper;
private final int mPositionOffset;
LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource,
@PageResult.ResultType int resultType, int positionOffset,
Executor mainThreadExecutor, PageResult.Receiver receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, resultType, mainThreadExecutor, receiver);
mPositionOffset = positionOffset;
}
@Override
public void onResult(@NonNull List data) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
data, 0, 0, mPositionOffset));
}
}
}
final void dispatchLoadInitial(boolean acceptCount,
int requestedStartPosition, int requestedLoadSize, int pageSize,
@NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) {
LoadInitialCallbackImpl 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);
}
final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
int count, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
LoadRangeCallback callback = new LoadRangeCallbackImpl<>(
this, resultType, startPosition, mainThreadExecutor, receiver);
if (count == 0) {
callback.onResult(Collections.emptyList());
} else {
loadRange(new LoadRangeParams(startPosition, count), callback);
}
}
/**
* Load initial list data.
*
* This method is called to load the initial page(s) from the DataSource.
*
* Result list must be a multiple of pageSize to enable efficient tiling.
*
* @param params Parameters for initial load, including requested start position, load size, and
* page size.
* @param callback Callback that receives initial load data, including
* position and total data set size.
*/
@WorkerThread
public abstract void loadInitial(
@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback callback);
/**
* Called to load a range of data from the DataSource.
*
* This method is called to load additional pages from the DataSource after the
* LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
*
* Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return
* the number of items requested, at the position requested.
*
* @param params Parameters for load, including start position and load size.
* @param callback Callback that receives loaded data.
*/
@WorkerThread
public abstract void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback callback);
@Override
boolean isContiguous() {
return false;
}
@NonNull
ContiguousDataSource wrapAsContiguousWithoutPlaceholders() {
return new ContiguousWithoutPlaceholdersWrapper<>(this);
}
/**
* Helper for computing an initial position in
* {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
* computed ahead of loading.
*
* The value computed by this function will do bounds checking, page alignment, and positioning
* based on initial load size requested.
*
* Example usage in a PositionalDataSource subclass:
*
* class ItemDataSource extends PositionalDataSource<Item> {
* private int computeCount() {
* // actual count code here
* }
*
* private List<Item> loadRangeInternal(int startPosition, int loadCount) {
* // actual load code here
* }
*
* {@literal @}Override
* public void loadInitial({@literal @}NonNull LoadInitialParams params,
* {@literal @}NonNull LoadInitialCallback<Item> callback) {
* int totalCount = computeCount();
* int position = computeInitialLoadPosition(params, totalCount);
* int loadSize = computeInitialLoadSize(params, position, totalCount);
* callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
* }
*
* {@literal @}Override
* public void loadRange({@literal @}NonNull LoadRangeParams params,
* {@literal @}NonNull LoadRangeCallback<Item> callback) {
* callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
* }
* }
*
* @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
* including page size, and requested start/loadSize.
* @param totalCount Total size of the data set.
* @return Position to start loading at.
*
* @see #computeInitialLoadSize(LoadInitialParams, int, int)
*/
public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
int totalCount) {
int position = params.requestedStartPosition;
int initialLoadSize = params.requestedLoadSize;
int pageSize = params.pageSize;
int roundedPageStart = Math.round(position / pageSize) * pageSize;
// maximum start pos is that which will encompass end of list
int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize;
roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
// minimum start position is 0
roundedPageStart = Math.max(0, roundedPageStart);
return roundedPageStart;
}
/**
* Helper for computing an initial load size in
* {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
* computed ahead of loading.
*
* This function takes the requested load size, and bounds checks it against the value returned
* by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
*
* Example usage in a PositionalDataSource subclass:
*
* class ItemDataSource extends PositionalDataSource<Item> {
* private int computeCount() {
* // actual count code here
* }
*
* private List<Item> loadRangeInternal(int startPosition, int loadCount) {
* // actual load code here
* }
*
* {@literal @}Override
* public void loadInitial({@literal @}NonNull LoadInitialParams params,
* {@literal @}NonNull LoadInitialCallback<Item> callback) {
* int totalCount = computeCount();
* int position = computeInitialLoadPosition(params, totalCount);
* int loadSize = computeInitialLoadSize(params, position, totalCount);
* callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
* }
*
* {@literal @}Override
* public void loadRange({@literal @}NonNull LoadRangeParams params,
* {@literal @}NonNull LoadRangeCallback<Item> callback) {
* callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
* }
* }
*
* @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
* including page size, and requested start/loadSize.
* @param initialLoadPosition Value returned by
* {@link #computeInitialLoadPosition(LoadInitialParams, int)}
* @param totalCount Total size of the data set.
* @return Number of items to load.
*
* @see #computeInitialLoadPosition(LoadInitialParams, int)
*/
@SuppressWarnings("WeakerAccess")
public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
int initialLoadPosition, int totalCount) {
return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize);
}
@SuppressWarnings("deprecation")
static class ContiguousWithoutPlaceholdersWrapper
extends ContiguousDataSource {
@NonNull
final PositionalDataSource mSource;
ContiguousWithoutPlaceholdersWrapper(
@NonNull PositionalDataSource source) {
mSource = source;
}
@Override
public void addInvalidatedCallback(
@NonNull InvalidatedCallback onInvalidatedCallback) {
mSource.addInvalidatedCallback(onInvalidatedCallback);
}
@Override
public void removeInvalidatedCallback(
@NonNull InvalidatedCallback onInvalidatedCallback) {
mSource.removeInvalidatedCallback(onInvalidatedCallback);
}
@Override
public void invalidate() {
mSource.invalidate();
}
@Override
public boolean isInvalid() {
return mSource.isInvalid();
}
@NonNull
@Override
public DataSource mapByPage(
@NonNull Function, List> function) {
throw new UnsupportedOperationException(
"Inaccessible inner type doesn't support map op");
}
@NonNull
@Override
public DataSource map(
@NonNull Function function) {
throw new UnsupportedOperationException(
"Inaccessible inner type doesn't support map op");
}
@Override
void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
final int convertPosition = position == null ? 0 : position;
// Note enablePlaceholders will be false here, but we don't have a way to communicate
// this to PositionalDataSource. This is fine, because only the list and its position
// offset will be consumed by the LoadInitialCallback.
mSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
pageSize, mainThreadExecutor, receiver);
}
@Override
void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
@NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
int startIndex = currentEndIndex + 1;
mSource.dispatchLoadRange(
PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
}
@Override
void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver receiver) {
int startIndex = currentBeginIndex - 1;
if (startIndex < 0) {
// trigger empty list load
mSource.dispatchLoadRange(
PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
} else {
int loadSize = Math.min(pageSize, startIndex + 1);
startIndex = startIndex - loadSize + 1;
mSource.dispatchLoadRange(
PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
}
}
@Override
Integer getKey(int position, Value item) {
return position;
}
}
@NonNull
@Override
public final PositionalDataSource mapByPage(
@NonNull Function, List> function) {
return new WrapperPositionalDataSource<>(this, function);
}
@NonNull
@Override
public final PositionalDataSource map(@NonNull Function function) {
return mapByPage(createListFunction(function));
}
}