| /* |
| * Copyright 2017 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 android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.WorkerThread; |
| |
| 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. |
| * <p> |
| * 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. |
| * <p> |
| * 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)}. |
| * <p> |
| * Room can generate a Factory of PositionalDataSources for you: |
| * <pre> |
| * {@literal @}Dao |
| * interface UserDao { |
| * {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC") |
| * public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc(); |
| * }</pre> |
| * |
| * @param <T> Type of items being loaded by the PositionalDataSource. |
| */ |
| public abstract class PositionalDataSource<T> extends DataSource<Integer, T> { |
| |
| /** |
| * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static class LoadInitialParams { |
| /** |
| * Initial load position requested. |
| * <p> |
| * 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. |
| * <p> |
| * Note that this may be larger than available data. |
| */ |
| public final int requestedLoadSize; |
| |
| /** |
| * Defines page size acceptable for return values. |
| * <p> |
| * 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; |
| |
| 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. |
| * <p> |
| * Returned data must start at this position. |
| */ |
| public final int startPosition; |
| /** |
| * Number of items to load. |
| * <p> |
| * Returned data must be of this size, unless at end of the list. |
| */ |
| public final int loadSize; |
| |
| LoadRangeParams(int startPosition, int loadSize) { |
| this.startPosition = startPosition; |
| this.loadSize = loadSize; |
| } |
| } |
| |
| /** |
| * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} |
| * to return data, position, and count. |
| * <p> |
| * A callback can be called only once, and will throw if called again. |
| * <p> |
| * 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 <T> Type of items being loaded. |
| */ |
| public static class LoadInitialCallback<T> extends BaseLoadCallback<T> { |
| private final boolean mCountingEnabled; |
| private final int mPageSize; |
| |
| LoadInitialCallback(@NonNull PositionalDataSource dataSource, boolean countingEnabled, |
| int pageSize, PageResult.Receiver<T> receiver) { |
| super(dataSource, PageResult.INIT, null, receiver); |
| mCountingEnabled = countingEnabled; |
| mPageSize = pageSize; |
| if (mPageSize < 1) { |
| throw new IllegalArgumentException("Page size must be non-negative"); |
| } |
| } |
| |
| /** |
| * Called to pass initial load state from a DataSource. |
| * <p> |
| * 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 void onResult(@NonNull List<T> data, int position, int totalCount) { |
| if (!dispatchInvalidResultIfInvalid()) { |
| 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(); |
| dispatchResultToReceiver( |
| new PageResult<>(data, position, trailingUnloadedCount, 0)); |
| } else { |
| // Only occurs when wrapped as contiguous |
| dispatchResultToReceiver(new PageResult<>(data, position)); |
| } |
| } |
| } |
| |
| /** |
| * Called to pass initial load state from a DataSource without total count, |
| * when placeholders aren't requested. |
| * <p class="note"><strong>Note:</strong> This method can only be called when placeholders |
| * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false). |
| * <p> |
| * 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}. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public void onResult(@NonNull List<T> data, int position) { |
| if (!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"); |
| } |
| dispatchResultToReceiver(new PageResult<>(data, position)); |
| } |
| } |
| } |
| |
| /** |
| * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)} |
| * to return data. |
| * <p> |
| * A callback can be called only once, and will throw if called again. |
| * <p> |
| * 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 <T> Type of items being loaded. |
| */ |
| public static class LoadRangeCallback<T> extends BaseLoadCallback<T> { |
| private final int mPositionOffset; |
| LoadRangeCallback(@NonNull PositionalDataSource dataSource, |
| @PageResult.ResultType int resultType, int positionOffset, |
| Executor mainThreadExecutor, PageResult.Receiver<T> receiver) { |
| super(dataSource, resultType, mainThreadExecutor, receiver); |
| mPositionOffset = positionOffset; |
| } |
| |
| /** |
| * 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 void onResult(@NonNull List<T> data) { |
| if (!dispatchInvalidResultIfInvalid()) { |
| dispatchResultToReceiver(new PageResult<>( |
| data, 0, 0, mPositionOffset)); |
| } |
| } |
| } |
| |
| final void dispatchLoadInitial(boolean acceptCount, |
| int requestedStartPosition, int requestedLoadSize, int pageSize, |
| @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) { |
| LoadInitialCallback<T> callback = |
| new LoadInitialCallback<>(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.setPostExecutor(mainThreadExecutor); |
| } |
| |
| final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition, |
| int count, @NonNull Executor mainThreadExecutor, |
| @NonNull PageResult.Receiver<T> receiver) { |
| LoadRangeCallback<T> callback = new LoadRangeCallback<>( |
| this, resultType, startPosition, mainThreadExecutor, receiver); |
| if (count == 0) { |
| callback.onResult(Collections.<T>emptyList()); |
| } else { |
| loadRange(new LoadRangeParams(startPosition, count), callback); |
| } |
| } |
| |
| /** |
| * Load initial list data. |
| * <p> |
| * This method is called to load the initial page(s) from the DataSource. |
| * <p> |
| * 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<T> callback); |
| |
| /** |
| * Called to load a range of data from the DataSource. |
| * <p> |
| * This method is called to load additional pages from the DataSource after the |
| * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList. |
| * <p> |
| * 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<T> callback); |
| |
| @Override |
| boolean isContiguous() { |
| return false; |
| } |
| |
| @NonNull |
| ContiguousDataSource<Integer, T> 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. |
| * <p> |
| * The value computed by this function will do bounds checking, page alignment, and positioning |
| * based on initial load size requested. |
| * <p> |
| * Example usage in a PositionalDataSource subclass: |
| * <pre> |
| * 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)); |
| * } |
| * }</pre> |
| * |
| * @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. |
| * <p> |
| * This function takes the requested load size, and bounds checks it against the value returned |
| * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}. |
| * <p> |
| * Example usage in a PositionalDataSource subclass: |
| * <pre> |
| * 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)); |
| * } |
| * }</pre> |
| * |
| * @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<Value> |
| extends ContiguousDataSource<Integer, Value> { |
| |
| @NonNull |
| final PositionalDataSource<Value> mPositionalDataSource; |
| |
| ContiguousWithoutPlaceholdersWrapper( |
| @NonNull PositionalDataSource<Value> positionalDataSource) { |
| mPositionalDataSource = positionalDataSource; |
| } |
| |
| @Override |
| void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize, |
| boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, |
| @NonNull PageResult.Receiver<Value> 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. |
| mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize, |
| pageSize, mainThreadExecutor, receiver); |
| } |
| |
| @Override |
| void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, |
| @NonNull Executor mainThreadExecutor, |
| @NonNull PageResult.Receiver<Value> receiver) { |
| int startIndex = currentEndIndex + 1; |
| mPositionalDataSource.dispatchLoadRange( |
| PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver); |
| } |
| |
| @Override |
| void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, |
| int pageSize, @NonNull Executor mainThreadExecutor, |
| @NonNull PageResult.Receiver<Value> receiver) { |
| |
| int startIndex = currentBeginIndex - 1; |
| if (startIndex < 0) { |
| // trigger empty list load |
| mPositionalDataSource.dispatchLoadRange( |
| PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver); |
| } else { |
| int loadSize = Math.min(pageSize, startIndex + 1); |
| startIndex = startIndex - loadSize + 1; |
| mPositionalDataSource.dispatchLoadRange( |
| PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver); |
| } |
| } |
| |
| @Override |
| Integer getKey(int position, Value item) { |
| return position; |
| } |
| } |
| } |