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.AnyThread; 20 import androidx.annotation.MainThread; 21 import androidx.annotation.NonNull; 22 import androidx.annotation.Nullable; 23 import androidx.annotation.RestrictTo; 24 import androidx.annotation.WorkerThread; 25 26 import java.lang.ref.WeakReference; 27 import java.util.AbstractList; 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.concurrent.Executor; 31 import java.util.concurrent.atomic.AtomicBoolean; 32 33 /** 34 * Lazy loading list that pages in immutable content from a {@link DataSource}. 35 * <p> 36 * A PagedList is a {@link List} which loads its data in chunks (pages) from a {@link DataSource}. 37 * Items can be accessed with {@link #get(int)}, and further loading can be triggered with 38 * {@link #loadAround(int)}. To display a PagedList, see {@link PagedListAdapter}, which enables the 39 * binding of a PagedList to a {@link androidx.recyclerview.widget.RecyclerView}. 40 * <h4>Loading Data</h4> 41 * <p> 42 * All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads the 43 * first chunk of data from the DataSource immediately, and should for this reason be done on a 44 * background thread. The constructed PagedList may then be passed to and used on the UI thread. 45 * This is done to prevent passing a list with no loaded content to the UI thread, which should 46 * generally not be presented to the user. 47 * <p> 48 * A PagedList initially presents this first partial load as its content, and expands over time as 49 * content is loaded in. When {@link #loadAround} is called, items will be loaded in near the passed 50 * list index. If placeholder {@code null}s are present in the list, they will be replaced as 51 * content is loaded. If not, newly loaded items will be inserted at the beginning or end of the 52 * list. 53 * <p> 54 * PagedList can present data for an unbounded, infinite scrolling list, or a very large but 55 * countable list. Use {@link Config} to control how many items a PagedList loads, and when. 56 * <p> 57 * If you use {@link LivePagedListBuilder} to get a 58 * {@link androidx.lifecycle.LiveData}<PagedList>, it will initialize PagedLists on a 59 * background thread for you. 60 * <h4>Placeholders</h4> 61 * <p> 62 * There are two ways that PagedList can represent its not-yet-loaded data - with or without 63 * {@code null} placeholders. 64 * <p> 65 * With placeholders, the PagedList is always the full size of the data set. {@code get(N)} returns 66 * the {@code N}th item in the data set, or {@code null} if its not yet loaded. 67 * <p> 68 * Without {@code null} placeholders, the PagedList is the sublist of data that has already been 69 * loaded. The size of the PagedList is the number of currently loaded items, and {@code get(N)} 70 * returns the {@code N}th <em>loaded</em> item. This is not necessarily the {@code N}th item in the 71 * data set. 72 * <p> 73 * Placeholders have several benefits: 74 * <ul> 75 * <li>They express the full sized list to the presentation layer (often a 76 * {@link PagedListAdapter}), and so can support scrollbars (without jumping as pages are 77 * loaded) and fast-scrolling to any position, whether loaded or not. 78 * <li>They avoid the need for a loading spinner at the end of the loaded list, since the list 79 * is always full sized. 80 * </ul> 81 * <p> 82 * They also have drawbacks: 83 * <ul> 84 * <li>Your Adapter (or other presentation mechanism) needs to account for {@code null} items. 85 * This often means providing default values in data you bind to a 86 * {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}. 87 * <li>They don't work well if your item views are of different sizes, as this will prevent 88 * loading items from cross-fading nicely. 89 * <li>They require you to count your data set, which can be expensive or impossible, depending 90 * on where your data comes from. 91 * </ul> 92 * <p> 93 * Placeholders are enabled by default, but can be disabled in two ways. They are disabled if the 94 * DataSource does not count its data set in its initial load, or if {@code false} is passed to 95 * {@link Config.Builder#setEnablePlaceholders(boolean)} when building a {@link Config}. 96 * <h4>Mutability and Snapshots</h4> 97 * A PagedList is <em>mutable</em> while loading, or ready to load from its DataSource. 98 * As loads succeed, a mutable PagedList will be updated via Runnables on the main thread. You can 99 * listen to these updates with a {@link Callback}. (Note that {@link PagedListAdapter} will listen 100 * to these to signal RecyclerView about the updates/changes). 101 * <p> 102 * If a PagedList attempts to load from an invalid DataSource, it will {@link #detach()} 103 * from the DataSource, meaning that it will no longer attempt to load data. It will return true 104 * from {@link #isImmutable()}, and a new DataSource / PagedList pair must be created to load 105 * further data. See {@link DataSource} and {@link LivePagedListBuilder} for how new PagedLists are 106 * created to represent changed data. 107 * <p> 108 * A PagedList snapshot is simply an immutable shallow copy of the current state of the PagedList as 109 * a {@code List}. It will reference the same inner items, and contain the same {@code null} 110 * placeholders, if present. 111 * 112 * @param <T> The type of the entries in the list. 113 */ 114 public abstract class PagedList<T> extends AbstractList<T> { 115 @NonNull 116 final Executor mMainThreadExecutor; 117 @NonNull 118 final Executor mBackgroundThreadExecutor; 119 @Nullable 120 final BoundaryCallback<T> mBoundaryCallback; 121 @NonNull 122 final Config mConfig; 123 @NonNull 124 final PagedStorage<T> mStorage; 125 126 int mLastLoad = 0; 127 T mLastItem = null; 128 129 // if set to true, mBoundaryCallback is non-null, and should 130 // be dispatched when nearby load has occurred 131 private boolean mBoundaryCallbackBeginDeferred = false; 132 private boolean mBoundaryCallbackEndDeferred = false; 133 134 // lowest and highest index accessed by loadAround. Used to 135 // decide when mBoundaryCallback should be dispatched 136 private int mLowestIndexAccessed = Integer.MAX_VALUE; 137 private int mHighestIndexAccessed = Integer.MIN_VALUE; 138 139 private final AtomicBoolean mDetached = new AtomicBoolean(false); 140 141 private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>(); 142 PagedList(@onNull PagedStorage<T> storage, @NonNull Executor mainThreadExecutor, @NonNull Executor backgroundThreadExecutor, @Nullable BoundaryCallback<T> boundaryCallback, @NonNull Config config)143 PagedList(@NonNull PagedStorage<T> storage, 144 @NonNull Executor mainThreadExecutor, 145 @NonNull Executor backgroundThreadExecutor, 146 @Nullable BoundaryCallback<T> boundaryCallback, 147 @NonNull Config config) { 148 mStorage = storage; 149 mMainThreadExecutor = mainThreadExecutor; 150 mBackgroundThreadExecutor = backgroundThreadExecutor; 151 mBoundaryCallback = boundaryCallback; 152 mConfig = config; 153 } 154 155 /** 156 * Create a PagedList which loads data from the provided data source on a background thread, 157 * posting updates to the main thread. 158 * 159 * 160 * @param dataSource DataSource providing data to the PagedList 161 * @param notifyExecutor Thread that will use and consume data from the PagedList. 162 * Generally, this is the UI/main thread. 163 * @param fetchExecutor Data loading will be done via this executor - 164 * should be a background thread. 165 * @param boundaryCallback Optional boundary callback to attach to the list. 166 * @param config PagedList Config, which defines how the PagedList will load data. 167 * @param <K> Key type that indicates to the DataSource what data to load. 168 * @param <T> Type of items to be held and loaded by the PagedList. 169 * 170 * @return Newly created PagedList, which will page in data from the DataSource as needed. 171 */ 172 @NonNull create(@onNull DataSource<K, T> dataSource, @NonNull Executor notifyExecutor, @NonNull Executor fetchExecutor, @Nullable BoundaryCallback<T> boundaryCallback, @NonNull Config config, @Nullable K key)173 private static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource, 174 @NonNull Executor notifyExecutor, 175 @NonNull Executor fetchExecutor, 176 @Nullable BoundaryCallback<T> boundaryCallback, 177 @NonNull Config config, 178 @Nullable K key) { 179 if (dataSource.isContiguous() || !config.enablePlaceholders) { 180 int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED; 181 if (!dataSource.isContiguous()) { 182 //noinspection unchecked 183 dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource) 184 .wrapAsContiguousWithoutPlaceholders(); 185 if (key != null) { 186 lastLoad = (int) key; 187 } 188 } 189 ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource; 190 return new ContiguousPagedList<>(contigDataSource, 191 notifyExecutor, 192 fetchExecutor, 193 boundaryCallback, 194 config, 195 key, 196 lastLoad); 197 } else { 198 return new TiledPagedList<>((PositionalDataSource<T>) dataSource, 199 notifyExecutor, 200 fetchExecutor, 201 boundaryCallback, 202 config, 203 (key != null) ? (Integer) key : 0); 204 } 205 } 206 207 /** 208 * Builder class for PagedList. 209 * <p> 210 * DataSource, Config, main thread and background executor must all be provided. 211 * <p> 212 * A PagedList queries initial data from its DataSource during construction, to avoid empty 213 * PagedLists being presented to the UI when possible. It's preferred to present initial data, 214 * so that the UI doesn't show an empty list, or placeholders for a few frames, just before 215 * showing initial content. 216 * <p> 217 * {@link LivePagedListBuilder} does this creation on a background thread automatically, if you 218 * want to receive a {@code LiveData<PagedList<...>>}. 219 * 220 * @param <Key> Type of key used to load data from the DataSource. 221 * @param <Value> Type of items held and loaded by the PagedList. 222 */ 223 @SuppressWarnings("WeakerAccess") 224 public static final class Builder<Key, Value> { 225 private final DataSource<Key, Value> mDataSource; 226 private final Config mConfig; 227 private Executor mNotifyExecutor; 228 private Executor mFetchExecutor; 229 private BoundaryCallback mBoundaryCallback; 230 private Key mInitialKey; 231 232 /** 233 * Create a PagedList.Builder with the provided {@link DataSource} and {@link Config}. 234 * 235 * @param dataSource DataSource the PagedList will load from. 236 * @param config Config that defines how the PagedList loads data from its DataSource. 237 */ Builder(@onNull DataSource<Key, Value> dataSource, @NonNull Config config)238 public Builder(@NonNull DataSource<Key, Value> dataSource, @NonNull Config config) { 239 //noinspection ConstantConditions 240 if (dataSource == null) { 241 throw new IllegalArgumentException("DataSource may not be null"); 242 } 243 //noinspection ConstantConditions 244 if (config == null) { 245 throw new IllegalArgumentException("Config may not be null"); 246 } 247 mDataSource = dataSource; 248 mConfig = config; 249 } 250 251 /** 252 * Create a PagedList.Builder with the provided {@link DataSource} and page size. 253 * <p> 254 * This method is a convenience for: 255 * <pre> 256 * PagedList.Builder(dataSource, 257 * new PagedList.Config.Builder().setPageSize(pageSize).build()); 258 * </pre> 259 * 260 * @param dataSource DataSource the PagedList will load from. 261 * @param pageSize Config that defines how the PagedList loads data from its DataSource. 262 */ Builder(@onNull DataSource<Key, Value> dataSource, int pageSize)263 public Builder(@NonNull DataSource<Key, Value> dataSource, int pageSize) { 264 this(dataSource, new PagedList.Config.Builder().setPageSize(pageSize).build()); 265 } 266 /** 267 * The executor defining where page loading updates are dispatched. 268 * 269 * @param notifyExecutor Executor that receives PagedList updates, and where 270 * {@link Callback} calls are dispatched. Generally, this is the ui/main thread. 271 * @return this 272 */ 273 @NonNull setNotifyExecutor(@onNull Executor notifyExecutor)274 public Builder<Key, Value> setNotifyExecutor(@NonNull Executor notifyExecutor) { 275 mNotifyExecutor = notifyExecutor; 276 return this; 277 } 278 279 /** 280 * The executor used to fetch additional pages from the DataSource. 281 * 282 * Does not affect initial load, which will be done immediately on whichever thread the 283 * PagedList is created on. 284 * 285 * @param fetchExecutor Executor used to fetch from DataSources, generally a background 286 * thread pool for e.g. I/O or network loading. 287 * @return this 288 */ 289 @NonNull setFetchExecutor(@onNull Executor fetchExecutor)290 public Builder<Key, Value> setFetchExecutor(@NonNull Executor fetchExecutor) { 291 mFetchExecutor = fetchExecutor; 292 return this; 293 } 294 295 /** 296 * The BoundaryCallback for out of data events. 297 * <p> 298 * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. 299 * 300 * @param boundaryCallback BoundaryCallback for listening to out-of-data events. 301 * @return this 302 */ 303 @SuppressWarnings("unused") 304 @NonNull setBoundaryCallback( @ullable BoundaryCallback boundaryCallback)305 public Builder<Key, Value> setBoundaryCallback( 306 @Nullable BoundaryCallback boundaryCallback) { 307 mBoundaryCallback = boundaryCallback; 308 return this; 309 } 310 311 /** 312 * Sets the initial key the DataSource should load around as part of initialization. 313 * 314 * @param initialKey Key the DataSource should load around as part of initialization. 315 * @return this 316 */ 317 @NonNull setInitialKey(@ullable Key initialKey)318 public Builder<Key, Value> setInitialKey(@Nullable Key initialKey) { 319 mInitialKey = initialKey; 320 return this; 321 } 322 323 /** 324 * Creates a {@link PagedList} with the given parameters. 325 * <p> 326 * This call will dispatch the {@link DataSource}'s loadInitial method immediately. If a 327 * DataSource posts all of its work (e.g. to a network thread), the PagedList will 328 * be immediately created as empty, and grow to its initial size when the initial load 329 * completes. 330 * <p> 331 * If the DataSource implements its load synchronously, doing the load work immediately in 332 * the loadInitial method, the PagedList will block on that load before completing 333 * construction. In this case, use a background thread to create a PagedList. 334 * <p> 335 * It's fine to create a PagedList with an async DataSource on the main thread, such as in 336 * the constructor of a ViewModel. An async network load won't block the initialLoad 337 * function. For a synchronous DataSource such as one created from a Room database, a 338 * {@code LiveData<PagedList>} can be safely constructed with {@link LivePagedListBuilder} 339 * on the main thread, since actual construction work is deferred, and done on a background 340 * thread. 341 * <p> 342 * While build() will always return a PagedList, it's important to note that the PagedList 343 * initial load may fail to acquire data from the DataSource. This can happen for example if 344 * the DataSource is invalidated during its initial load. If this happens, the PagedList 345 * will be immediately {@link PagedList#isDetached() detached}, and you can retry 346 * construction (including setting a new DataSource). 347 * 348 * @return The newly constructed PagedList 349 */ 350 @WorkerThread 351 @NonNull build()352 public PagedList<Value> build() { 353 // TODO: define defaults, once they can be used in module without android dependency 354 if (mNotifyExecutor == null) { 355 throw new IllegalArgumentException("MainThreadExecutor required"); 356 } 357 if (mFetchExecutor == null) { 358 throw new IllegalArgumentException("BackgroundThreadExecutor required"); 359 } 360 361 //noinspection unchecked 362 return PagedList.create( 363 mDataSource, 364 mNotifyExecutor, 365 mFetchExecutor, 366 mBoundaryCallback, 367 mConfig, 368 mInitialKey); 369 } 370 } 371 372 /** 373 * Get the item in the list of loaded items at the provided index. 374 * 375 * @param index Index in the loaded item list. Must be >= 0, and < {@link #size()} 376 * @return The item at the passed index, or null if a null placeholder is at the specified 377 * position. 378 * 379 * @see #size() 380 */ 381 @Override 382 @Nullable get(int index)383 public T get(int index) { 384 T item = mStorage.get(index); 385 if (item != null) { 386 mLastItem = item; 387 } 388 return item; 389 } 390 391 /** 392 * Load adjacent items to passed index. 393 * 394 * @param index Index at which to load. 395 */ loadAround(int index)396 public void loadAround(int index) { 397 mLastLoad = index + getPositionOffset(); 398 loadAroundInternal(index); 399 400 mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index); 401 mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index); 402 403 /* 404 * mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to 405 * dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded, 406 * and accesses happen near the boundaries. 407 * 408 * Note: we post here, since RecyclerView may want to add items in response, and this 409 * call occurs in PagedListAdapter bind. 410 */ 411 tryDispatchBoundaryCallbacks(true); 412 } 413 414 // Creation thread for initial synchronous load, otherwise main thread 415 // Safe to access main thread only state - no other thread has reference during construction 416 @AnyThread deferBoundaryCallbacks(final boolean deferEmpty, final boolean deferBegin, final boolean deferEnd)417 void deferBoundaryCallbacks(final boolean deferEmpty, 418 final boolean deferBegin, final boolean deferEnd) { 419 if (mBoundaryCallback == null) { 420 throw new IllegalStateException("Can't defer BoundaryCallback, no instance"); 421 } 422 423 /* 424 * If lowest/highest haven't been initialized, set them to storage size, 425 * since placeholders must already be computed by this point. 426 * 427 * This is just a minor optimization so that BoundaryCallback callbacks are sent immediately 428 * if the initial load size is smaller than the prefetch window (see 429 * TiledPagedListTest#boundaryCallback_immediate()) 430 */ 431 if (mLowestIndexAccessed == Integer.MAX_VALUE) { 432 mLowestIndexAccessed = mStorage.size(); 433 } 434 if (mHighestIndexAccessed == Integer.MIN_VALUE) { 435 mHighestIndexAccessed = 0; 436 } 437 438 if (deferEmpty || deferBegin || deferEnd) { 439 // Post to the main thread, since we may be on creation thread currently 440 mMainThreadExecutor.execute(new Runnable() { 441 @Override 442 public void run() { 443 // on is dispatched immediately, since items won't be accessed 444 //noinspection ConstantConditions 445 if (deferEmpty) { 446 mBoundaryCallback.onZeroItemsLoaded(); 447 } 448 449 // for other callbacks, mark deferred, and only dispatch if loadAround 450 // has been called near to the position 451 if (deferBegin) { 452 mBoundaryCallbackBeginDeferred = true; 453 } 454 if (deferEnd) { 455 mBoundaryCallbackEndDeferred = true; 456 } 457 tryDispatchBoundaryCallbacks(false); 458 } 459 }); 460 } 461 } 462 463 /** 464 * Call this when mLowest/HighestIndexAccessed are changed, or 465 * mBoundaryCallbackBegin/EndDeferred is set. 466 */ tryDispatchBoundaryCallbacks(boolean post)467 private void tryDispatchBoundaryCallbacks(boolean post) { 468 final boolean dispatchBegin = mBoundaryCallbackBeginDeferred 469 && mLowestIndexAccessed <= mConfig.prefetchDistance; 470 final boolean dispatchEnd = mBoundaryCallbackEndDeferred 471 && mHighestIndexAccessed >= size() - 1 - mConfig.prefetchDistance; 472 473 if (!dispatchBegin && !dispatchEnd) { 474 return; 475 } 476 477 if (dispatchBegin) { 478 mBoundaryCallbackBeginDeferred = false; 479 } 480 if (dispatchEnd) { 481 mBoundaryCallbackEndDeferred = false; 482 } 483 if (post) { 484 mMainThreadExecutor.execute(new Runnable() { 485 @Override 486 public void run() { 487 dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd); 488 } 489 }); 490 } else { 491 dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd); 492 } 493 } 494 dispatchBoundaryCallbacks(boolean begin, boolean end)495 private void dispatchBoundaryCallbacks(boolean begin, boolean end) { 496 // safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present 497 if (begin) { 498 //noinspection ConstantConditions 499 mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem()); 500 } 501 if (end) { 502 //noinspection ConstantConditions 503 mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem()); 504 } 505 } 506 507 /** @hide */ 508 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) offsetBoundaryAccessIndices(int offset)509 void offsetBoundaryAccessIndices(int offset) { 510 mLowestIndexAccessed += offset; 511 mHighestIndexAccessed += offset; 512 } 513 514 /** 515 * Returns size of the list, including any not-yet-loaded null padding. 516 * 517 * @return Current total size of the list. 518 */ 519 @Override size()520 public int size() { 521 return mStorage.size(); 522 } 523 524 /** 525 * Returns whether the list is immutable. 526 * 527 * Immutable lists may not become mutable again, and may safely be accessed from any thread. 528 * <p> 529 * In the future, this method may return true when a PagedList has completed loading from its 530 * DataSource. Currently, it is equivalent to {@link #isDetached()}. 531 * 532 * @return True if the PagedList is immutable. 533 */ 534 @SuppressWarnings("WeakerAccess") isImmutable()535 public boolean isImmutable() { 536 return isDetached(); 537 } 538 539 /** 540 * Returns an immutable snapshot of the PagedList in its current state. 541 * 542 * If this PagedList {@link #isImmutable() is immutable} due to its DataSource being invalid, it 543 * will be returned. 544 * 545 * @return Immutable snapshot of PagedList data. 546 */ 547 @SuppressWarnings("WeakerAccess") 548 @NonNull snapshot()549 public List<T> snapshot() { 550 if (isImmutable()) { 551 return this; 552 } 553 return new SnapshotPagedList<>(this); 554 } 555 isContiguous()556 abstract boolean isContiguous(); 557 558 /** 559 * Return the Config used to construct this PagedList. 560 * 561 * @return the Config of this PagedList 562 */ 563 @NonNull getConfig()564 public Config getConfig() { 565 return mConfig; 566 } 567 568 /** 569 * Return the DataSource that provides data to this PagedList. 570 * 571 * @return the DataSource of this PagedList. 572 */ 573 @NonNull getDataSource()574 public abstract DataSource<?, T> getDataSource(); 575 576 /** 577 * Return the key for the position passed most recently to {@link #loadAround(int)}. 578 * <p> 579 * When a PagedList is invalidated, you can pass the key returned by this function to initialize 580 * the next PagedList. This ensures (depending on load times) that the next PagedList that 581 * arrives will have data that overlaps. If you use {@link LivePagedListBuilder}, it will do 582 * this for you. 583 * 584 * @return Key of position most recently passed to {@link #loadAround(int)}. 585 */ 586 @Nullable getLastKey()587 public abstract Object getLastKey(); 588 589 /** 590 * True if the PagedList has detached the DataSource it was loading from, and will no longer 591 * load new data. 592 * <p> 593 * A detached list is {@link #isImmutable() immutable}. 594 * 595 * @return True if the data source is detached. 596 */ 597 @SuppressWarnings("WeakerAccess") isDetached()598 public boolean isDetached() { 599 return mDetached.get(); 600 } 601 602 /** 603 * Detach the PagedList from its DataSource, and attempt to load no more data. 604 * <p> 605 * This is called automatically when a DataSource load returns <code>null</code>, which is a 606 * signal to stop loading. The PagedList will continue to present existing data, but will not 607 * initiate new loads. 608 */ 609 @SuppressWarnings("WeakerAccess") detach()610 public void detach() { 611 mDetached.set(true); 612 } 613 614 /** 615 * Position offset of the data in the list. 616 * <p> 617 * If data is supplied by a {@link PositionalDataSource}, the item returned from 618 * <code>get(i)</code> has a position of <code>i + getPositionOffset()</code>. 619 * <p> 620 * If the DataSource is a {@link ItemKeyedDataSource} or {@link PageKeyedDataSource}, it 621 * doesn't use positions, returns 0. 622 */ getPositionOffset()623 public int getPositionOffset() { 624 return mStorage.getPositionOffset(); 625 } 626 627 /** 628 * Adds a callback, and issues updates since the previousSnapshot was created. 629 * <p> 630 * If previousSnapshot is passed, the callback will also immediately be dispatched any 631 * differences between the previous snapshot, and the current state. For example, if the 632 * previousSnapshot was of 5 nulls, 10 items, 5 nulls, and the current state was 5 nulls, 633 * 12 items, 3 nulls, the callback would immediately receive a call of 634 * <code>onChanged(14, 2)</code>. 635 * <p> 636 * This allows an observer that's currently presenting a snapshot to catch up to the most recent 637 * version, including any changes that may have been made. 638 * <p> 639 * The callback is internally held as weak reference, so PagedList doesn't hold a strong 640 * reference to its observer, such as a {@link PagedListAdapter}. If an adapter were held with a 641 * strong reference, it would be necessary to clear its PagedList observer before it could be 642 * GC'd. 643 * 644 * @param previousSnapshot Snapshot previously captured from this List, or null. 645 * @param callback Callback to dispatch to. 646 * 647 * @see #removeWeakCallback(Callback) 648 */ 649 @SuppressWarnings("WeakerAccess") addWeakCallback(@ullable List<T> previousSnapshot, @NonNull Callback callback)650 public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) { 651 if (previousSnapshot != null && previousSnapshot != this) { 652 653 if (previousSnapshot.isEmpty()) { 654 if (!mStorage.isEmpty()) { 655 // If snapshot is empty, diff is trivial - just notify number new items. 656 // Note: occurs in async init, when snapshot taken before init page arrives 657 callback.onInserted(0, mStorage.size()); 658 } 659 } else { 660 PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot; 661 662 //noinspection unchecked 663 dispatchUpdatesSinceSnapshot(storageSnapshot, callback); 664 } 665 } 666 667 // first, clean up any empty weak refs 668 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 669 Callback currentCallback = mCallbacks.get(i).get(); 670 if (currentCallback == null) { 671 mCallbacks.remove(i); 672 } 673 } 674 675 // then add the new one 676 mCallbacks.add(new WeakReference<>(callback)); 677 } 678 /** 679 * Removes a previously added callback. 680 * 681 * @param callback Callback, previously added. 682 * @see #addWeakCallback(List, Callback) 683 */ 684 @SuppressWarnings("WeakerAccess") removeWeakCallback(@onNull Callback callback)685 public void removeWeakCallback(@NonNull Callback callback) { 686 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 687 Callback currentCallback = mCallbacks.get(i).get(); 688 if (currentCallback == null || currentCallback == callback) { 689 // found callback, or empty weak ref 690 mCallbacks.remove(i); 691 } 692 } 693 } 694 notifyInserted(int position, int count)695 void notifyInserted(int position, int count) { 696 if (count != 0) { 697 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 698 Callback callback = mCallbacks.get(i).get(); 699 if (callback != null) { 700 callback.onInserted(position, count); 701 } 702 } 703 } 704 } 705 notifyChanged(int position, int count)706 void notifyChanged(int position, int count) { 707 if (count != 0) { 708 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 709 Callback callback = mCallbacks.get(i).get(); 710 711 if (callback != null) { 712 callback.onChanged(position, count); 713 } 714 } 715 } 716 } 717 718 719 720 /** 721 * Dispatch updates since the non-empty snapshot was taken. 722 * 723 * @param snapshot Non-empty snapshot. 724 * @param callback Callback for updates that have occurred since snapshot. 725 */ dispatchUpdatesSinceSnapshot(@onNull PagedList<T> snapshot, @NonNull Callback callback)726 abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot, 727 @NonNull Callback callback); 728 loadAroundInternal(int index)729 abstract void loadAroundInternal(int index); 730 731 /** 732 * Callback signaling when content is loaded into the list. 733 * <p> 734 * Can be used to listen to items being paged in and out. These calls will be dispatched on 735 * the executor defined by {@link Builder#setNotifyExecutor(Executor)}, which is generally 736 * the main/UI thread. 737 */ 738 public abstract static class Callback { 739 /** 740 * Called when null padding items have been loaded to signal newly available data, or when 741 * data that hasn't been used in a while has been dropped, and swapped back to null. 742 * 743 * @param position Position of first newly loaded items, out of total number of items 744 * (including padded nulls). 745 * @param count Number of items loaded. 746 */ onChanged(int position, int count)747 public abstract void onChanged(int position, int count); 748 749 /** 750 * Called when new items have been loaded at the end or beginning of the list. 751 * 752 * @param position Position of the first newly loaded item (in practice, either 753 * <code>0</code> or <code>size - 1</code>. 754 * @param count Number of items loaded. 755 */ onInserted(int position, int count)756 public abstract void onInserted(int position, int count); 757 758 /** 759 * Called when items have been removed at the end or beginning of the list, and have not 760 * been replaced by padded nulls. 761 * 762 * @param position Position of the first newly loaded item (in practice, either 763 * <code>0</code> or <code>size - 1</code>. 764 * @param count Number of items loaded. 765 */ 766 @SuppressWarnings("unused") onRemoved(int position, int count)767 public abstract void onRemoved(int position, int count); 768 } 769 770 /** 771 * Configures how a PagedList loads content from its DataSource. 772 * <p> 773 * Use a Config {@link Builder} to construct and define custom loading behavior, such as 774 * {@link Builder#setPageSize(int)}, which defines number of items loaded at a time}. 775 */ 776 public static class Config { 777 /** 778 * Size of each page loaded by the PagedList. 779 */ 780 public final int pageSize; 781 782 /** 783 * Prefetch distance which defines how far ahead to load. 784 * <p> 785 * If this value is set to 50, the paged list will attempt to load 50 items in advance of 786 * data that's already been accessed. 787 * 788 * @see PagedList#loadAround(int) 789 */ 790 @SuppressWarnings("WeakerAccess") 791 public final int prefetchDistance; 792 793 /** 794 * Defines whether the PagedList may display null placeholders, if the DataSource provides 795 * them. 796 */ 797 @SuppressWarnings("WeakerAccess") 798 public final boolean enablePlaceholders; 799 800 /** 801 * Size hint for initial load of PagedList, often larger than a regular page. 802 */ 803 @SuppressWarnings("WeakerAccess") 804 public final int initialLoadSizeHint; 805 Config(int pageSize, int prefetchDistance, boolean enablePlaceholders, int initialLoadSizeHint)806 private Config(int pageSize, int prefetchDistance, 807 boolean enablePlaceholders, int initialLoadSizeHint) { 808 this.pageSize = pageSize; 809 this.prefetchDistance = prefetchDistance; 810 this.enablePlaceholders = enablePlaceholders; 811 this.initialLoadSizeHint = initialLoadSizeHint; 812 } 813 814 /** 815 * Builder class for {@link Config}. 816 * <p> 817 * You must at minimum specify page size with {@link #setPageSize(int)}. 818 */ 819 public static final class Builder { 820 private int mPageSize = -1; 821 private int mPrefetchDistance = -1; 822 private int mInitialLoadSizeHint = -1; 823 private boolean mEnablePlaceholders = true; 824 825 /** 826 * Defines the number of items loaded at once from the DataSource. 827 * <p> 828 * Should be several times the number of visible items onscreen. 829 * <p> 830 * Configuring your page size depends on how your data is being loaded and used. Smaller 831 * page sizes improve memory usage, latency, and avoid GC churn. Larger pages generally 832 * improve loading throughput, to a point 833 * (avoid loading more than 2MB from SQLite at once, since it incurs extra cost). 834 * <p> 835 * If you're loading data for very large, social-media style cards that take up most of 836 * a screen, and your database isn't a bottleneck, 10-20 may make sense. If you're 837 * displaying dozens of items in a tiled grid, which can present items during a scroll 838 * much more quickly, consider closer to 100. 839 * 840 * @param pageSize Number of items loaded at once from the DataSource. 841 * @return this 842 */ setPageSize(int pageSize)843 public Builder setPageSize(int pageSize) { 844 this.mPageSize = pageSize; 845 return this; 846 } 847 848 /** 849 * Defines how far from the edge of loaded content an access must be to trigger further 850 * loading. 851 * <p> 852 * Should be several times the number of visible items onscreen. 853 * <p> 854 * If not set, defaults to page size. 855 * <p> 856 * A value of 0 indicates that no list items will be loaded until they are specifically 857 * requested. This is generally not recommended, so that users don't observe a 858 * placeholder item (with placeholders) or end of list (without) while scrolling. 859 * 860 * @param prefetchDistance Distance the PagedList should prefetch. 861 * @return this 862 */ setPrefetchDistance(int prefetchDistance)863 public Builder setPrefetchDistance(int prefetchDistance) { 864 this.mPrefetchDistance = prefetchDistance; 865 return this; 866 } 867 868 /** 869 * Pass false to disable null placeholders in PagedLists using this Config. 870 * <p> 871 * If not set, defaults to true. 872 * <p> 873 * A PagedList will present null placeholders for not-yet-loaded content if two 874 * conditions are met: 875 * <p> 876 * 1) Its DataSource can count all unloaded items (so that the number of nulls to 877 * present is known). 878 * <p> 879 * 2) placeholders are not disabled on the Config. 880 * <p> 881 * Call {@code setEnablePlaceholders(false)} to ensure the receiver of the PagedList 882 * (often a {@link PagedListAdapter}) doesn't need to account for null items. 883 * <p> 884 * If placeholders are disabled, not-yet-loaded content will not be present in the list. 885 * Paging will still occur, but as items are loaded or removed, they will be signaled 886 * as inserts to the {@link PagedList.Callback}. 887 * {@link PagedList.Callback#onChanged(int, int)} will not be issued as part of loading, 888 * though a {@link PagedListAdapter} may still receive change events as a result of 889 * PagedList diffing. 890 * 891 * @param enablePlaceholders False if null placeholders should be disabled. 892 * @return this 893 */ 894 @SuppressWarnings("SameParameterValue") setEnablePlaceholders(boolean enablePlaceholders)895 public Builder setEnablePlaceholders(boolean enablePlaceholders) { 896 this.mEnablePlaceholders = enablePlaceholders; 897 return this; 898 } 899 900 /** 901 * Defines how many items to load when first load occurs. 902 * <p> 903 * This value is typically larger than page size, so on first load data there's a large 904 * enough range of content loaded to cover small scrolls. 905 * <p> 906 * When using a {@link PositionalDataSource}, the initial load size will be coerced to 907 * an integer multiple of pageSize, to enable efficient tiling. 908 * <p> 909 * If not set, defaults to three times page size. 910 * 911 * @param initialLoadSizeHint Number of items to load while initializing the PagedList. 912 * @return this 913 */ 914 @SuppressWarnings("WeakerAccess") setInitialLoadSizeHint(int initialLoadSizeHint)915 public Builder setInitialLoadSizeHint(int initialLoadSizeHint) { 916 this.mInitialLoadSizeHint = initialLoadSizeHint; 917 return this; 918 } 919 920 /** 921 * Creates a {@link Config} with the given parameters. 922 * 923 * @return A new Config. 924 */ build()925 public Config build() { 926 if (mPageSize < 1) { 927 throw new IllegalArgumentException("Page size must be a positive number"); 928 } 929 if (mPrefetchDistance < 0) { 930 mPrefetchDistance = mPageSize; 931 } 932 if (mInitialLoadSizeHint < 0) { 933 mInitialLoadSizeHint = mPageSize * 3; 934 } 935 if (!mEnablePlaceholders && mPrefetchDistance == 0) { 936 throw new IllegalArgumentException("Placeholders and prefetch are the only ways" 937 + " to trigger loading of more data in the PagedList, so either" 938 + " placeholders must be enabled, or prefetch distance must be > 0."); 939 } 940 941 return new Config(mPageSize, mPrefetchDistance, 942 mEnablePlaceholders, mInitialLoadSizeHint); 943 } 944 } 945 } 946 947 /** 948 * Signals when a PagedList has reached the end of available data. 949 * <p> 950 * When local storage is a cache of network data, it's common to set up a streaming pipeline: 951 * Network data is paged into the database, database is paged into UI. Paging from the database 952 * to UI can be done with a {@code LiveData<PagedList>}, but it's still necessary to know when 953 * to trigger network loads. 954 * <p> 955 * BoundaryCallback does this signaling - when a DataSource runs out of data at the end of 956 * the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network 957 * load that will write the result directly to the database. Because the database is being 958 * observed, the UI bound to the {@code LiveData<PagedList>} will update automatically to 959 * account for the new items. 960 * <p> 961 * Note that a BoundaryCallback instance shared across multiple PagedLists (e.g. when passed to 962 * {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple 963 * times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should 964 * avoid triggering it again while the load is ongoing. 965 * <p> 966 * BoundaryCallback only passes the item at front or end of the list. Number of items is not 967 * passed, since it may not be fully computed by the DataSource if placeholders are not 968 * supplied. Keys are not known because the BoundaryCallback is independent of the 969 * DataSource-specific keys, which may be different for local vs remote storage. 970 * <p> 971 * The database + network Repository in the 972 * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a> 973 * shows how to implement a network BoundaryCallback using 974 * <a href="https://square.github.io/retrofit/">Retrofit</a>, while 975 * handling swipe-to-refresh, network errors, and retry. 976 * 977 * @param <T> Type loaded by the PagedList. 978 */ 979 @MainThread 980 public abstract static class BoundaryCallback<T> { 981 /** 982 * Called when zero items are returned from an initial load of the PagedList's data source. 983 */ onZeroItemsLoaded()984 public void onZeroItemsLoaded() {} 985 986 /** 987 * Called when the item at the front of the PagedList has been loaded, and access has 988 * occurred within {@link Config#prefetchDistance} of it. 989 * <p> 990 * No more data will be prepended to the PagedList before this item. 991 * 992 * @param itemAtFront The first item of PagedList 993 */ onItemAtFrontLoaded(@onNull T itemAtFront)994 public void onItemAtFrontLoaded(@NonNull T itemAtFront) {} 995 996 /** 997 * Called when the item at the end of the PagedList has been loaded, and access has 998 * occurred within {@link Config#prefetchDistance} of it. 999 * <p> 1000 * No more data will be appended to the PagedList after this item. 1001 * 1002 * @param itemAtEnd The first item of PagedList 1003 */ onItemAtEndLoaded(@onNull T itemAtEnd)1004 public void onItemAtEndLoaded(@NonNull T itemAtEnd) {} 1005 } 1006 } 1007