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}&lt;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 &lt; {@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