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 
LoadInitialParams( int requestedStartPosition, int requestedLoadSize, int pageSize, boolean placeholdersEnabled)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 
LoadRangeParams(int startPosition, int loadSize)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          */
onResult(@onNull List<T> data, int position, int totalCount)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          */
onResult(@onNull List<T> data, int position)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          */
onResult(@onNull List<T> data)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 
LoadInitialCallbackImpl(@onNull PositionalDataSource dataSource, boolean countingEnabled, int pageSize, PageResult.Receiver<T> receiver)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
onResult(@onNull List<T> data, int position, int totalCount)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
onResult(@onNull List<T> data, int position)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;
LoadRangeCallbackImpl(@onNull PositionalDataSource dataSource, @PageResult.ResultType int resultType, int positionOffset, Executor mainThreadExecutor, PageResult.Receiver<T> receiver)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
onResult(@onNull List<T> data)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 
dispatchLoadInitial(boolean acceptCount, int requestedStartPosition, int requestedLoadSize, int pageSize, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver)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 
dispatchLoadRange(@ageResult.ResultType int resultType, int startPosition, int count, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver)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
loadInitial( @onNull LoadInitialParams params, @NonNull LoadInitialCallback<T> callback)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
loadRange(@onNull LoadRangeParams params, @NonNull LoadRangeCallback<T> callback)336     public abstract void loadRange(@NonNull LoadRangeParams params,
337             @NonNull LoadRangeCallback<T> callback);
338 
339     @Override
isContiguous()340     boolean isContiguous() {
341         return false;
342     }
343 
344     @NonNull
wrapAsContiguousWithoutPlaceholders()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      */
computeInitialLoadPosition(@onNull LoadInitialParams params, int totalCount)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")
computeInitialLoadSize(@onNull LoadInitialParams params, int initialLoadPosition, int totalCount)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 
ContiguousWithoutPlaceholdersWrapper( @onNull PositionalDataSource<Value> source)465         ContiguousWithoutPlaceholdersWrapper(
466                 @NonNull PositionalDataSource<Value> source) {
467             mSource = source;
468         }
469 
470         @Override
addInvalidatedCallback( @onNull InvalidatedCallback onInvalidatedCallback)471         public void addInvalidatedCallback(
472                 @NonNull InvalidatedCallback onInvalidatedCallback) {
473             mSource.addInvalidatedCallback(onInvalidatedCallback);
474         }
475 
476         @Override
removeInvalidatedCallback( @onNull InvalidatedCallback onInvalidatedCallback)477         public void removeInvalidatedCallback(
478                 @NonNull InvalidatedCallback onInvalidatedCallback) {
479             mSource.removeInvalidatedCallback(onInvalidatedCallback);
480         }
481 
482         @Override
invalidate()483         public void invalidate() {
484             mSource.invalidate();
485         }
486 
487         @Override
isInvalid()488         public boolean isInvalid() {
489             return mSource.isInvalid();
490         }
491 
492         @NonNull
493         @Override
mapByPage( @onNull Function<List<Value>, List<ToValue>> function)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
map( @onNull Function<Value, ToValue> function)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
dispatchLoadInitial(@ullable Integer position, int initialLoadSize, int pageSize, boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver)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
dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver)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
dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver)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
getKey(int position, Value item)549         Integer getKey(int position, Value item) {
550             return position;
551         }
552 
553     }
554 
555     @NonNull
556     @Override
mapByPage( @onNull Function<List<T>, List<V>> function)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
map(@onNull Function<T, V> function)564     public final <V> PositionalDataSource<V> map(@NonNull Function<T, V> function) {
565         return mapByPage(createListFunction(function));
566     }
567 }
568