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<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<Item> { 360 * private int computeCount() { 361 * // actual count code here 362 * } 363 * 364 * private List<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<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<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<Item> { 420 * private int computeCount() { 421 * // actual count code here 422 * } 423 * 424 * private List<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<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<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