Home | History | Annotate | Download | only in paging
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.paging;
     18 
     19 import androidx.annotation.NonNull;
     20 import androidx.annotation.Nullable;
     21 import androidx.annotation.WorkerThread;
     22 import androidx.arch.core.util.Function;
     23 
     24 import java.util.Collections;
     25 import java.util.List;
     26 import java.util.concurrent.Executor;
     27 
     28 /**
     29  * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
     30  * arbitrary page positions.
     31  * <p>
     32  * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
     33  * positions, and provide a fixed item count. If your data source can't support loading arbitrary
     34  * requested page sizes (e.g. when network page size constraints are only known at runtime), use
     35  * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
     36  * <p>
     37  * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
     38  * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
     39  * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
     40  * If placeholders are disabled, initialize with the two parameter
     41  * {@link LoadInitialCallback#onResult(List, int)}.
     42  * <p>
     43  * Room can generate a Factory of PositionalDataSources for you:
     44  * <pre>
     45  * {@literal @}Dao
     46  * interface UserDao {
     47  *     {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
     48  *     public abstract DataSource.Factory&lt;Integer, User> loadUsersByAgeDesc();
     49  * }</pre>
     50  *
     51  * @param <T> Type of items being loaded by the PositionalDataSource.
     52  */
     53 public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
     54 
     55     /**
     56      * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
     57      */
     58     @SuppressWarnings("WeakerAccess")
     59     public static class LoadInitialParams {
     60         /**
     61          * Initial load position requested.
     62          * <p>
     63          * Note that this may not be within the bounds of your data set, it may need to be adjusted
     64          * before you execute your load.
     65          */
     66         public final int requestedStartPosition;
     67 
     68         /**
     69          * Requested number of items to load.
     70          * <p>
     71          * Note that this may be larger than available data.
     72          */
     73         public final int requestedLoadSize;
     74 
     75         /**
     76          * Defines page size acceptable for return values.
     77          * <p>
     78          * List of items passed to the callback must be an integer multiple of page size.
     79          */
     80         public final int pageSize;
     81 
     82         /**
     83          * Defines whether placeholders are enabled, and whether the total count passed to
     84          * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
     85          */
     86         public final boolean placeholdersEnabled;
     87 
     88         public LoadInitialParams(
     89                 int requestedStartPosition,
     90                 int requestedLoadSize,
     91                 int pageSize,
     92                 boolean placeholdersEnabled) {
     93             this.requestedStartPosition = requestedStartPosition;
     94             this.requestedLoadSize = requestedLoadSize;
     95             this.pageSize = pageSize;
     96             this.placeholdersEnabled = placeholdersEnabled;
     97         }
     98     }
     99 
    100     /**
    101      * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
    102      */
    103     @SuppressWarnings("WeakerAccess")
    104     public static class LoadRangeParams {
    105         /**
    106          * Start position of data to load.
    107          * <p>
    108          * Returned data must start at this position.
    109          */
    110         public final int startPosition;
    111         /**
    112          * Number of items to load.
    113          * <p>
    114          * Returned data must be of this size, unless at end of the list.
    115          */
    116         public final int loadSize;
    117 
    118         public LoadRangeParams(int startPosition, int loadSize) {
    119             this.startPosition = startPosition;
    120             this.loadSize = loadSize;
    121         }
    122     }
    123 
    124     /**
    125      * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
    126      * to return data, position, and count.
    127      * <p>
    128      * A callback should be called only once, and may throw if called again.
    129      * <p>
    130      * It is always valid for a DataSource loading method that takes a callback to stash the
    131      * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
    132      * temporary, recoverable error states (such as a network error that can be retried).
    133      *
    134      * @param <T> Type of items being loaded.
    135      */
    136     public abstract static class LoadInitialCallback<T> {
    137         /**
    138          * Called to pass initial load state from a DataSource.
    139          * <p>
    140          * Call this method from your DataSource's {@code loadInitial} function to return data,
    141          * and inform how many placeholders should be shown before and after. If counting is cheap
    142          * to compute (for example, if a network load returns the information regardless), it's
    143          * recommended to pass the total size to the totalCount parameter. If placeholders are not
    144          * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
    145          * call {@link #onResult(List, int)}.
    146          *
    147          * @param data List of items loaded from the DataSource. If this is empty, the DataSource
    148          *             is treated as empty, and no further loads will occur.
    149          * @param position Position of the item at the front of the list. If there are {@code N}
    150          *                 items before the items in data that can be loaded from this DataSource,
    151          *                 pass {@code N}.
    152          * @param totalCount Total number of items that may be returned from this DataSource.
    153          *                   Includes the number in the initial {@code data} parameter
    154          *                   as well as any items that can be loaded in front or behind of
    155          *                   {@code data}.
    156          */
    157         public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
    158 
    159         /**
    160          * Called to pass initial load state from a DataSource without total count,
    161          * when placeholders aren't requested.
    162          * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
    163          * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
    164          * <p>
    165          * Call this method from your DataSource's {@code loadInitial} function to return data,
    166          * if position is known but total size is not. If placeholders are requested, call the three
    167          * parameter variant: {@link #onResult(List, int, int)}.
    168          *
    169          * @param data List of items loaded from the DataSource. If this is empty, the DataSource
    170          *             is treated as empty, and no further loads will occur.
    171          * @param position Position of the item at the front of the list. If there are {@code N}
    172          *                 items before the items in data that can be provided by this DataSource,
    173          *                 pass {@code N}.
    174          */
    175         public abstract void onResult(@NonNull List<T> data, int position);
    176     }
    177 
    178     /**
    179      * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
    180      * to return data.
    181      * <p>
    182      * A callback should be called only once, and may throw if called again.
    183      * <p>
    184      * It is always valid for a DataSource loading method that takes a callback to stash the
    185      * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
    186      * temporary, recoverable error states (such as a network error that can be retried).
    187      *
    188      * @param <T> Type of items being loaded.
    189      */
    190     public abstract static class LoadRangeCallback<T> {
    191         /**
    192          * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
    193          *
    194          * @param data List of items loaded from the DataSource. Must be same size as requested,
    195          *             unless at end of list.
    196          */
    197         public abstract void onResult(@NonNull List<T> data);
    198     }
    199 
    200     static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
    201         final LoadCallbackHelper<T> mCallbackHelper;
    202         private final boolean mCountingEnabled;
    203         private final int mPageSize;
    204 
    205         LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
    206                 int pageSize, PageResult.Receiver<T> receiver) {
    207             mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
    208             mCountingEnabled = countingEnabled;
    209             mPageSize = pageSize;
    210             if (mPageSize < 1) {
    211                 throw new IllegalArgumentException("Page size must be non-negative");
    212             }
    213         }
    214 
    215         @Override
    216         public void onResult(@NonNull List<T> data, int position, int totalCount) {
    217             if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
    218                 LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
    219                 if (position + data.size() != totalCount
    220                         && data.size() % mPageSize != 0) {
    221                     throw new IllegalArgumentException("PositionalDataSource requires initial load"
    222                             + " size to be a multiple of page size to support internal tiling."
    223                             + " loadSize " + data.size() + ", position " + position
    224                             + ", totalCount " + totalCount + ", pageSize " + mPageSize);
    225                 }
    226 
    227                 if (mCountingEnabled) {
    228                     int trailingUnloadedCount = totalCount - position - data.size();
    229                     mCallbackHelper.dispatchResultToReceiver(
    230                             new PageResult<>(data, position, trailingUnloadedCount, 0));
    231                 } else {
    232                     // Only occurs when wrapped as contiguous
    233                     mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
    234                 }
    235             }
    236         }
    237 
    238         @Override
    239         public void onResult(@NonNull List<T> data, int position) {
    240             if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
    241                 if (position < 0) {
    242                     throw new IllegalArgumentException("Position must be non-negative");
    243                 }
    244                 if (data.isEmpty() && position != 0) {
    245                     throw new IllegalArgumentException(
    246                             "Initial result cannot be empty if items are present in data set.");
    247                 }
    248                 if (mCountingEnabled) {
    249                     throw new IllegalStateException("Placeholders requested, but totalCount not"
    250                             + " provided. Please call the three-parameter onResult method, or"
    251                             + " disable placeholders in the PagedList.Config");
    252                 }
    253                 mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
    254             }
    255         }
    256     }
    257 
    258     static class LoadRangeCallbackImpl<T> extends LoadRangeCallback<T> {
    259         private LoadCallbackHelper<T> mCallbackHelper;
    260         private final int mPositionOffset;
    261         LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource,
    262                 @PageResult.ResultType int resultType, int positionOffset,
    263                 Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
    264             mCallbackHelper = new LoadCallbackHelper<>(
    265                     dataSource, resultType, mainThreadExecutor, receiver);
    266             mPositionOffset = positionOffset;
    267         }
    268 
    269         @Override
    270         public void onResult(@NonNull List<T> data) {
    271             if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
    272                 mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
    273                         data, 0, 0, mPositionOffset));
    274             }
    275         }
    276     }
    277 
    278     final void dispatchLoadInitial(boolean acceptCount,
    279             int requestedStartPosition, int requestedLoadSize, int pageSize,
    280             @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
    281         LoadInitialCallbackImpl<T> callback =
    282                 new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
    283 
    284         LoadInitialParams params = new LoadInitialParams(
    285                 requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
    286         loadInitial(params, callback);
    287 
    288         // If initialLoad's callback is not called within the body, we force any following calls
    289         // to post to the UI thread. This constructor may be run on a background thread, but
    290         // after constructor, mutation must happen on UI thread.
    291         callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
    292     }
    293 
    294     final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
    295             int count, @NonNull Executor mainThreadExecutor,
    296             @NonNull PageResult.Receiver<T> receiver) {
    297         LoadRangeCallback<T> callback = new LoadRangeCallbackImpl<>(
    298                 this, resultType, startPosition, mainThreadExecutor, receiver);
    299         if (count == 0) {
    300             callback.onResult(Collections.<T>emptyList());
    301         } else {
    302             loadRange(new LoadRangeParams(startPosition, count), callback);
    303         }
    304     }
    305 
    306     /**
    307      * Load initial list data.
    308      * <p>
    309      * This method is called to load the initial page(s) from the DataSource.
    310      * <p>
    311      * Result list must be a multiple of pageSize to enable efficient tiling.
    312      *
    313      * @param params Parameters for initial load, including requested start position, load size, and
    314      *               page size.
    315      * @param callback Callback that receives initial load data, including
    316      *                 position and total data set size.
    317      */
    318     @WorkerThread
    319     public abstract void loadInitial(
    320             @NonNull LoadInitialParams params,
    321             @NonNull LoadInitialCallback<T> callback);
    322 
    323     /**
    324      * Called to load a range of data from the DataSource.
    325      * <p>
    326      * This method is called to load additional pages from the DataSource after the
    327      * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
    328      * <p>
    329      * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return
    330      * the number of items requested, at the position requested.
    331      *
    332      * @param params Parameters for load, including start position and load size.
    333      * @param callback Callback that receives loaded data.
    334      */
    335     @WorkerThread
    336     public abstract void loadRange(@NonNull LoadRangeParams params,
    337             @NonNull LoadRangeCallback<T> callback);
    338 
    339     @Override
    340     boolean isContiguous() {
    341         return false;
    342     }
    343 
    344     @NonNull
    345     ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
    346         return new ContiguousWithoutPlaceholdersWrapper<>(this);
    347     }
    348 
    349     /**
    350      * Helper for computing an initial position in
    351      * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
    352      * computed ahead of loading.
    353      * <p>
    354      * The value computed by this function will do bounds checking, page alignment, and positioning
    355      * based on initial load size requested.
    356      * <p>
    357      * Example usage in a PositionalDataSource subclass:
    358      * <pre>
    359      * class ItemDataSource extends PositionalDataSource&lt;Item> {
    360      *     private int computeCount() {
    361      *         // actual count code here
    362      *     }
    363      *
    364      *     private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
    365      *         // actual load code here
    366      *     }
    367      *
    368      *     {@literal @}Override
    369      *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
    370      *             {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
    371      *         int totalCount = computeCount();
    372      *         int position = computeInitialLoadPosition(params, totalCount);
    373      *         int loadSize = computeInitialLoadSize(params, position, totalCount);
    374      *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
    375      *     }
    376      *
    377      *     {@literal @}Override
    378      *     public void loadRange({@literal @}NonNull LoadRangeParams params,
    379      *             {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
    380      *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
    381      *     }
    382      * }</pre>
    383      *
    384      * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
    385      *               including page size, and requested start/loadSize.
    386      * @param totalCount Total size of the data set.
    387      * @return Position to start loading at.
    388      *
    389      * @see #computeInitialLoadSize(LoadInitialParams, int, int)
    390      */
    391     public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
    392             int totalCount) {
    393         int position = params.requestedStartPosition;
    394         int initialLoadSize = params.requestedLoadSize;
    395         int pageSize = params.pageSize;
    396 
    397         int roundedPageStart = Math.round(position / pageSize) * pageSize;
    398 
    399         // maximum start pos is that which will encompass end of list
    400         int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize;
    401         roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
    402 
    403         // minimum start position is 0
    404         roundedPageStart = Math.max(0, roundedPageStart);
    405 
    406         return roundedPageStart;
    407     }
    408 
    409     /**
    410      * Helper for computing an initial load size in
    411      * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
    412      * computed ahead of loading.
    413      * <p>
    414      * This function takes the requested load size, and bounds checks it against the value returned
    415      * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
    416      * <p>
    417      * Example usage in a PositionalDataSource subclass:
    418      * <pre>
    419      * class ItemDataSource extends PositionalDataSource&lt;Item> {
    420      *     private int computeCount() {
    421      *         // actual count code here
    422      *     }
    423      *
    424      *     private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
    425      *         // actual load code here
    426      *     }
    427      *
    428      *     {@literal @}Override
    429      *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
    430      *             {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
    431      *         int totalCount = computeCount();
    432      *         int position = computeInitialLoadPosition(params, totalCount);
    433      *         int loadSize = computeInitialLoadSize(params, position, totalCount);
    434      *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
    435      *     }
    436      *
    437      *     {@literal @}Override
    438      *     public void loadRange({@literal @}NonNull LoadRangeParams params,
    439      *             {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
    440      *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
    441      *     }
    442      * }</pre>
    443      *
    444      * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
    445      *               including page size, and requested start/loadSize.
    446      * @param initialLoadPosition Value returned by
    447      *                          {@link #computeInitialLoadPosition(LoadInitialParams, int)}
    448      * @param totalCount Total size of the data set.
    449      * @return Number of items to load.
    450      *
    451      * @see #computeInitialLoadPosition(LoadInitialParams, int)
    452      */
    453     @SuppressWarnings("WeakerAccess")
    454     public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
    455             int initialLoadPosition, int totalCount) {
    456         return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize);
    457     }
    458 
    459     @SuppressWarnings("deprecation")
    460     static class ContiguousWithoutPlaceholdersWrapper<Value>
    461             extends ContiguousDataSource<Integer, Value> {
    462         @NonNull
    463         final PositionalDataSource<Value> mSource;
    464 
    465         ContiguousWithoutPlaceholdersWrapper(
    466                 @NonNull PositionalDataSource<Value> source) {
    467             mSource = source;
    468         }
    469 
    470         @Override
    471         public void addInvalidatedCallback(
    472                 @NonNull InvalidatedCallback onInvalidatedCallback) {
    473             mSource.addInvalidatedCallback(onInvalidatedCallback);
    474         }
    475 
    476         @Override
    477         public void removeInvalidatedCallback(
    478                 @NonNull InvalidatedCallback onInvalidatedCallback) {
    479             mSource.removeInvalidatedCallback(onInvalidatedCallback);
    480         }
    481 
    482         @Override
    483         public void invalidate() {
    484             mSource.invalidate();
    485         }
    486 
    487         @Override
    488         public boolean isInvalid() {
    489             return mSource.isInvalid();
    490         }
    491 
    492         @NonNull
    493         @Override
    494         public <ToValue> DataSource<Integer, ToValue> mapByPage(
    495                 @NonNull Function<List<Value>, List<ToValue>> function) {
    496             throw new UnsupportedOperationException(
    497                     "Inaccessible inner type doesn't support map op");
    498         }
    499 
    500         @NonNull
    501         @Override
    502         public <ToValue> DataSource<Integer, ToValue> map(
    503                 @NonNull Function<Value, ToValue> function) {
    504             throw new UnsupportedOperationException(
    505                     "Inaccessible inner type doesn't support map op");
    506         }
    507 
    508         @Override
    509         void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
    510                 boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
    511                 @NonNull PageResult.Receiver<Value> receiver) {
    512             final int convertPosition = position == null ? 0 : position;
    513 
    514             // Note enablePlaceholders will be false here, but we don't have a way to communicate
    515             // this to PositionalDataSource. This is fine, because only the list and its position
    516             // offset will be consumed by the LoadInitialCallback.
    517             mSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
    518                     pageSize, mainThreadExecutor, receiver);
    519         }
    520 
    521         @Override
    522         void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
    523                 @NonNull Executor mainThreadExecutor,
    524                 @NonNull PageResult.Receiver<Value> receiver) {
    525             int startIndex = currentEndIndex + 1;
    526             mSource.dispatchLoadRange(
    527                     PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
    528         }
    529 
    530         @Override
    531         void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
    532                 int pageSize, @NonNull Executor mainThreadExecutor,
    533                 @NonNull PageResult.Receiver<Value> receiver) {
    534 
    535             int startIndex = currentBeginIndex - 1;
    536             if (startIndex < 0) {
    537                 // trigger empty list load
    538                 mSource.dispatchLoadRange(
    539                         PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
    540             } else {
    541                 int loadSize = Math.min(pageSize, startIndex + 1);
    542                 startIndex = startIndex - loadSize + 1;
    543                 mSource.dispatchLoadRange(
    544                         PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
    545             }
    546         }
    547 
    548         @Override
    549         Integer getKey(int position, Value item) {
    550             return position;
    551         }
    552 
    553     }
    554 
    555     @NonNull
    556     @Override
    557     public final <V> PositionalDataSource<V> mapByPage(
    558             @NonNull Function<List<T>, List<V>> function) {
    559         return new WrapperPositionalDataSource<>(this, function);
    560     }
    561 
    562     @NonNull
    563     @Override
    564     public final <V> PositionalDataSource<V> map(@NonNull Function<T, V> function) {
    565         return mapByPage(createListFunction(function));
    566     }
    567 }
    568