blob: 4e17a1513c92e12dd1d42a48abe66fd75a298867 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.arch.paging;
import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.WorkerThread;
import java.lang.ref.WeakReference;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Lazy loading list that pages in content from a {@link DataSource}.
* <p>
* A PagedList is a {@link List} which loads its data in chunks (pages) from a {@link DataSource}.
* Items can be accessed with {@link #get(int)}, and further loading can be triggered with
* {@link #loadAround(int)}. See {@link PagedListAdapter}, which enables the binding of a PagedList
* to a {@link android.support.v7.widget.RecyclerView}.
* <h4>Loading Data</h4>
* <p>
* All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads data
* from the DataSource immediately, and should for this reason be done on a background thread. The
* constructed PagedList may then be passed to and used on the UI thread. This is done to prevent
* passing a list with no loaded content to the UI thread, which should generally not be presented
* to the user.
* <p>
* When {@link #loadAround} is called, items will be loaded in near the passed list index. If
* placeholder {@code null}s are present in the list, they will be replaced as content is
* loaded. If not, newly loaded items will be inserted at the beginning or end of the list.
* <p>
* PagedList can present data for an unbounded, infinite scrolling list, or a very large but
* countable list. Use {@link Config} to control how many items a PagedList loads, and when.
* <p>
* If you use {@link LivePagedListBuilder} to get a
* {@link android.arch.lifecycle.LiveData}&lt;PagedList>, it will initialize PagedLists on a
* background thread for you.
* <h4>Placeholders</h4>
* <p>
* There are two ways that PagedList can represent its not-yet-loaded data - with or without
* {@code null} placeholders.
* <p>
* With placeholders, the PagedList is always the full size of the data set. {@code get(N)} returns
* the {@code N}th item in the data set, or {@code null} if its not yet loaded.
* <p>
* Without {@code null} placeholders, the PagedList is the sublist of data that has already been
* loaded. The size of the PagedList is the number of currently loaded items, and {@code get(N)}
* returns the {@code N}th <em>loaded</em> item. This is not necessarily the {@code N}th item in the
* data set.
* <p>
* Placeholders have several benefits:
* <ul>
* <li>They express the full sized list to the presentation layer (often a
* {@link PagedListAdapter}), and so can support scrollbars (without jumping as pages are
* loaded) and fast-scrolling to any position, whether loaded or not.
* <li>They avoid the need for a loading spinner at the end of the loaded list, since the list
* is always full sized.
* </ul>
* <p>
* They also have drawbacks:
* <ul>
* <li>Your Adapter (or other presentation mechanism) needs to account for {@code null} items.
* This often means providing default values in data you bind to a
* {@link android.support.v7.widget.RecyclerView.ViewHolder}.
* <li>They don't work well if your item views are of different sizes, as this will prevent
* loading items from cross-fading nicely.
* <li>They require you to count your data set, which can be expensive or impossible, depending
* on where your data comes from.
* </ul>
* <p>
* Placeholders are enabled by default, but can be disabled in two ways. They are disabled if the
* DataSource does not count its data set in its initial load, or if {@code false} is passed to
* {@link Config.Builder#setEnablePlaceholders(boolean)} when building a {@link Config}.
*
* @param <T> The type of the entries in the list.
*/
public abstract class PagedList<T> extends AbstractList<T> {
@NonNull
final Executor mMainThreadExecutor;
@NonNull
final Executor mBackgroundThreadExecutor;
@Nullable
final BoundaryCallback<T> mBoundaryCallback;
@NonNull
final Config mConfig;
@NonNull
final PagedStorage<T> mStorage;
int mLastLoad = 0;
T mLastItem = null;
// if set to true, mBoundaryCallback is non-null, and should
// be dispatched when nearby load has occurred
private boolean mBoundaryCallbackBeginDeferred = false;
private boolean mBoundaryCallbackEndDeferred = false;
// lowest and highest index accessed by loadAround. Used to
// decide when mBoundaryCallback should be dispatched
private int mLowestIndexAccessed = Integer.MAX_VALUE;
private int mHighestIndexAccessed = Integer.MIN_VALUE;
private final AtomicBoolean mDetached = new AtomicBoolean(false);
protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
PagedList(@NonNull PagedStorage<T> storage,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config) {
mStorage = storage;
mMainThreadExecutor = mainThreadExecutor;
mBackgroundThreadExecutor = backgroundThreadExecutor;
mBoundaryCallback = boundaryCallback;
mConfig = config;
}
/**
* Create a PagedList which loads data from the provided data source on a background thread,
* posting updates to the main thread.
*
*
* @param dataSource DataSource providing data to the PagedList
* @param mainThreadExecutor Thread that will use and consume data from the PagedList.
* Generally, this is the UI/main thread.
* @param backgroundThreadExecutor Data loading will be done via this executor - should be a
* background thread.
* @param boundaryCallback Optional boundary callback to attach to the list.
* @param config PagedList Config, which defines how the PagedList will load data.
* @param <K> Key type that indicates to the DataSource what data to load.
* @param <T> Type of items to be held and loaded by the PagedList.
*
* @return Newly created PagedList, which will page in data from the DataSource as needed.
*/
@NonNull
private static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key) {
if (dataSource.isContiguous() || !config.enablePlaceholders) {
if (!dataSource.isContiguous()) {
//noinspection unchecked
dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
.wrapAsContiguousWithoutPlaceholders();
}
ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
return new ContiguousPagedList<>(contigDataSource,
mainThreadExecutor,
backgroundThreadExecutor,
boundaryCallback,
config,
key);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
mainThreadExecutor,
backgroundThreadExecutor,
boundaryCallback,
config,
(key != null) ? (Integer) key : 0);
}
}
/**
* Builder class for PagedList.
* <p>
* DataSource, Config, main thread and background executor must all be provided.
* <p>
* A PagedList queries initial data from its DataSource during construction, to avoid empty
* PagedLists being presented to the UI when possible. It's preferred to present initial data,
* so that the UI doesn't show an empty list, or placeholders for a few frames, just before
* showing initial content.
* <p>
* {@link LivePagedListBuilder} does this creation on a background thread automatically, if you
* want to receive a {@code LiveData<PagedList<...>>}.
*
* @param <Key> Type of key used to load data from the DataSource.
* @param <Value> Type of items held and loaded by the PagedList.
*/
@SuppressWarnings("WeakerAccess")
public static class Builder<Key, Value> {
private final DataSource<Key, Value> mDataSource;
private final Config mConfig;
private Executor mMainThreadExecutor;
private Executor mBackgroundThreadExecutor;
private BoundaryCallback mBoundaryCallback;
private Key mInitialKey;
/**
* Create a PagedList.Builder with the provided {@link DataSource} and {@link Config}.
*
* @param dataSource DataSource the PagedList will load from.
* @param config Config that defines how the PagedList loads data from its DataSource.
*/
public Builder(@NonNull DataSource<Key, Value> dataSource, @NonNull Config config) {
//noinspection ConstantConditions
if (dataSource == null) {
throw new IllegalArgumentException("DataSource may not be null");
}
//noinspection ConstantConditions
if (config == null) {
throw new IllegalArgumentException("Config may not be null");
}
mDataSource = dataSource;
mConfig = config;
}
/**
* Create a PagedList.Builder with the provided {@link DataSource} and page size.
* <p>
* This method is a convenience for:
* <pre>
* PagedList.Builder(dataSource,
* new PagedList.Config.Builder().setPageSize(pageSize).build());
* </pre>
*
* @param dataSource DataSource the PagedList will load from.
* @param pageSize Config that defines how the PagedList loads data from its DataSource.
*/
public Builder(@NonNull DataSource<Key, Value> dataSource, int pageSize) {
this(dataSource, new PagedList.Config.Builder().setPageSize(pageSize).build());
}
/**
* The executor defining where main/UI thread for page loading updates.
*
* @param mainThreadExecutor Executor for main/UI thread to receive {@link Callback} calls.
* @return this
*/
@NonNull
public Builder<Key, Value> setMainThreadExecutor(@NonNull Executor mainThreadExecutor) {
mMainThreadExecutor = mainThreadExecutor;
return this;
}
/**
* The executor on which background loading will be run.
* <p>
* Does not affect initial load, which will be done on whichever thread the PagedList is
* created on.
*
* @param backgroundThreadExecutor Executor for background DataSource loading.
* @return this
*/
@NonNull
public Builder<Key, Value> setBackgroundThreadExecutor(
@NonNull Executor backgroundThreadExecutor) {
mBackgroundThreadExecutor = backgroundThreadExecutor;
return this;
}
/**
* The BoundaryCallback for out of data events.
* <p>
* Pass a BoundaryCallback to listen to when the PagedList runs out of data to load.
*
* @param boundaryCallback BoundaryCallback for listening to out-of-data events.
* @return this
*/
@SuppressWarnings("unused")
@NonNull
public Builder<Key, Value> setBoundaryCallback(
@Nullable BoundaryCallback boundaryCallback) {
mBoundaryCallback = boundaryCallback;
return this;
}
/**
* Sets the initial key the DataSource should load around as part of initialization.
*
* @param initialKey Key the DataSource should load around as part of initialization.
* @return this
*/
@NonNull
public Builder<Key, Value> setInitialKey(@Nullable Key initialKey) {
mInitialKey = initialKey;
return this;
}
/**
* Creates a {@link PagedList} with the given parameters.
* <p>
* This call will dispatch the {@link DataSource}'s loadInitial method immediately. If a
* DataSource posts all of its work (e.g. to a network thread), the PagedList will
* be immediately created as empty, and grow to its initial size when the initial load
* completes.
* <p>
* If the DataSource implements its load synchronously, doing the load work immediately in
* the loadInitial method, the PagedList will block on that load before completing
* construction. In this case, use a background thread to create a PagedList.
* <p>
* It's fine to create a PagedList with an async DataSource on the main thread, such as in
* the constructor of a ViewModel. An async network load won't block the initialLoad
* function. For a synchronous DataSource such as one created from a Room database, a
* {@code LiveData<PagedList>} can be safely constructed with {@link LivePagedListBuilder}
* on the main thread, since actual construction work is deferred, and done on a background
* thread.
* <p>
* While build() will always return a PagedList, it's important to note that the PagedList
* initial load may fail to acquire data from the DataSource. This can happen for example if
* the DataSource is invalidated during its initial load. If this happens, the PagedList
* will be immediately {@link PagedList#isDetached() detached}, and you can retry
* construction (including setting a new DataSource).
*
* @return The newly constructed PagedList
*/
@WorkerThread
@NonNull
public PagedList<Value> build() {
// TODO: define defaults, once they can be used in module without android dependency
if (mMainThreadExecutor == null) {
throw new IllegalArgumentException("MainThreadExecutor required");
}
if (mBackgroundThreadExecutor == null) {
throw new IllegalArgumentException("BackgroundThreadExecutor required");
}
//noinspection unchecked
return PagedList.create(
mDataSource,
mMainThreadExecutor,
mBackgroundThreadExecutor,
mBoundaryCallback,
mConfig,
mInitialKey);
}
}
/**
* Get the item in the list of loaded items at the provided index.
*
* @param index Index in the loaded item list. Must be >= 0, and &lt; {@link #size()}
* @return The item at the passed index, or null if a null placeholder is at the specified
* position.
*
* @see #size()
*/
@Override
@Nullable
public T get(int index) {
T item = mStorage.get(index);
if (item != null) {
mLastItem = item;
}
return item;
}
/**
* Load adjacent items to passed index.
*
* @param index Index at which to load.
*/
public void loadAround(int index) {
mLastLoad = index + getPositionOffset();
loadAroundInternal(index);
mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
/*
* mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to
* dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
* and accesses happen near the boundaries.
*
* Note: we post here, since RecyclerView may want to add items in response, and this
* call occurs in PagedListAdapter bind.
*/
tryDispatchBoundaryCallbacks(true);
}
// Creation thread for initial synchronous load, otherwise main thread
// Safe to access main thread only state - no other thread has reference during construction
@AnyThread
void deferBoundaryCallbacks(final boolean deferEmpty,
final boolean deferBegin, final boolean deferEnd) {
if (mBoundaryCallback == null) {
throw new IllegalStateException("Computing boundary");
}
/*
* If lowest/highest haven't been initialized, set them to storage size,
* since placeholders must already be computed by this point.
*
* This is just a minor optimization so that BoundaryCallback callbacks are sent immediately
* if the initial load size is smaller than the prefetch window (see
* TiledPagedListTest#boundaryCallback_immediate())
*/
if (mLowestIndexAccessed == Integer.MAX_VALUE) {
mLowestIndexAccessed = mStorage.size();
}
if (mHighestIndexAccessed == Integer.MIN_VALUE) {
mHighestIndexAccessed = 0;
}
if (deferEmpty || deferBegin || deferEnd) {
// Post to the main thread, since we may be on creation thread currently
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
// on is dispatched immediately, since items won't be accessed
//noinspection ConstantConditions
if (deferEmpty) {
mBoundaryCallback.onZeroItemsLoaded();
}
// for other callbacks, mark deferred, and only dispatch if loadAround
// has been called near to the position
if (deferBegin) {
mBoundaryCallbackBeginDeferred = true;
}
if (deferEnd) {
mBoundaryCallbackEndDeferred = true;
}
tryDispatchBoundaryCallbacks(false);
}
});
}
}
/**
* Call this when mLowest/HighestIndexAccessed are changed, or
* mBoundaryCallbackBegin/EndDeferred is set.
*/
private void tryDispatchBoundaryCallbacks(boolean post) {
final boolean dispatchBegin = mBoundaryCallbackBeginDeferred
&& mLowestIndexAccessed <= mConfig.prefetchDistance;
final boolean dispatchEnd = mBoundaryCallbackEndDeferred
&& mHighestIndexAccessed >= size() - mConfig.prefetchDistance;
if (!dispatchBegin && !dispatchEnd) {
return;
}
if (dispatchBegin) {
mBoundaryCallbackBeginDeferred = false;
}
if (dispatchEnd) {
mBoundaryCallbackEndDeferred = false;
}
if (post) {
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
}
});
} else {
dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
}
}
private void dispatchBoundaryCallbacks(boolean begin, boolean end) {
// safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present
if (begin) {
//noinspection ConstantConditions
mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem());
}
if (end) {
//noinspection ConstantConditions
mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem());
}
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
void offsetBoundaryAccessIndices(int offset) {
mLowestIndexAccessed += offset;
mHighestIndexAccessed += offset;
}
/**
* Returns size of the list, including any not-yet-loaded null padding.
*
* @return Current total size of the list.
*/
@Override
public int size() {
return mStorage.size();
}
/**
* Returns whether the list is immutable. Immutable lists may not become mutable again, and may
* safely be accessed from any thread.
*
* @return True if the PagedList is immutable.
*/
@SuppressWarnings("WeakerAccess")
public boolean isImmutable() {
return isDetached();
}
/**
* Returns an immutable snapshot of the PagedList. If this PagedList is already
* immutable, it will be returned.
*
* @return Immutable snapshot of PagedList data.
*/
@SuppressWarnings("WeakerAccess")
@NonNull
public List<T> snapshot() {
if (isImmutable()) {
return this;
}
return new SnapshotPagedList<>(this);
}
abstract boolean isContiguous();
/**
* Return the Config used to construct this PagedList.
*
* @return the Config of this PagedList
*/
@NonNull
public Config getConfig() {
return mConfig;
}
/**
* Return the key for the position passed most recently to {@link #loadAround(int)}.
* <p>
* When a PagedList is invalidated, you can pass the key returned by this function to initialize
* the next PagedList. This ensures (depending on load times) that the next PagedList that
* arrives will have data that overlaps. If you use {@link LivePagedListBuilder}, it will do
* this for you.
*
* @return Key of position most recently passed to {@link #loadAround(int)}.
*/
@Nullable
public abstract Object getLastKey();
/**
* True if the PagedList has detached the DataSource it was loading from, and will no longer
* load new data.
*
* @return True if the data source is detached.
*/
@SuppressWarnings("WeakerAccess")
public boolean isDetached() {
return mDetached.get();
}
/**
* Detach the PagedList from its DataSource, and attempt to load no more data.
* <p>
* This is called automatically when a DataSource load returns <code>null</code>, which is a
* signal to stop loading. The PagedList will continue to present existing data, but will not
* initiate new loads.
*/
@SuppressWarnings("WeakerAccess")
public void detach() {
mDetached.set(true);
}
/**
* Position offset of the data in the list.
* <p>
* If data is supplied by a {@link PositionalDataSource}, the item returned from
* <code>get(i)</code> has a position of <code>i + getPositionOffset()</code>.
* <p>
* If the DataSource is a {@link KeyedDataSource}, and thus doesn't use positions, returns 0.
*/
public int getPositionOffset() {
return mStorage.getPositionOffset();
}
/**
* Adds a callback, and issues updates since the previousSnapshot was created.
* <p>
* If previousSnapshot is passed, the callback will also immediately be dispatched any
* differences between the previous snapshot, and the current state. For example, if the
* previousSnapshot was of 5 nulls, 10 items, 5 nulls, and the current state was 5 nulls,
* 12 items, 3 nulls, the callback would immediately receive a call of
* <code>onChanged(14, 2)</code>.
* <p>
* This allows an observer that's currently presenting a snapshot to catch up to the most recent
* version, including any changes that may have been made.
* <p>
* The callback is internally held as weak reference, so PagedList doesn't hold a strong
* reference to its observer, such as a {@link PagedListAdapter}. If an adapter were held with a
* strong reference, it would be necessary to clear its PagedList observer before it could be
* GC'd.
*
* @param previousSnapshot Snapshot previously captured from this List, or null.
* @param callback LoadCallback to dispatch to.
* @see #removeWeakCallback(Callback)
*/
@SuppressWarnings("WeakerAccess")
public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) {
if (previousSnapshot != null && previousSnapshot != this) {
if (previousSnapshot.isEmpty()) {
if (!mStorage.isEmpty()) {
// If snapshot is empty, diff is trivial - just notify number new items.
// Note: occurs in async init, when snapshot taken before init page arrives
callback.onInserted(0, mStorage.size());
}
} else {
PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot;
//noinspection unchecked
dispatchUpdatesSinceSnapshot(storageSnapshot, callback);
}
}
// first, clean up any empty weak refs
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback currentCallback = mCallbacks.get(i).get();
if (currentCallback == null) {
mCallbacks.remove(i);
}
}
// then add the new one
mCallbacks.add(new WeakReference<>(callback));
}
/**
* Removes a previously added callback.
*
* @param callback LoadCallback, previously added.
* @see #addWeakCallback(List, Callback)
*/
@SuppressWarnings("WeakerAccess")
public void removeWeakCallback(@NonNull Callback callback) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback currentCallback = mCallbacks.get(i).get();
if (currentCallback == null || currentCallback == callback) {
// found callback, or empty weak ref
mCallbacks.remove(i);
}
}
}
void notifyInserted(int position, int count) {
if (count != 0) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback callback = mCallbacks.get(i).get();
if (callback != null) {
callback.onInserted(position, count);
}
}
}
}
void notifyChanged(int position, int count) {
if (count != 0) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback callback = mCallbacks.get(i).get();
if (callback != null) {
callback.onChanged(position, count);
}
}
}
}
/**
* Dispatch updates since the non-empty snapshot was taken.
*
* @param snapshot Non-empty snapshot.
* @param callback LoadCallback for updates that have occurred since snapshot.
*/
abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot,
@NonNull Callback callback);
abstract void loadAroundInternal(int index);
/**
* Callback signaling when content is loaded into the list.
* <p>
* Can be used to listen to items being paged in and out. These calls will be dispatched on
* the executor defined by {@link Builder#setMainThreadExecutor(Executor)}, which defaults to
* the main/UI thread.
*/
public abstract static class Callback {
/**
* Called when null padding items have been loaded to signal newly available data, or when
* data that hasn't been used in a while has been dropped, and swapped back to null.
*
* @param position Position of first newly loaded items, out of total number of items
* (including padded nulls).
* @param count Number of items loaded.
*/
public abstract void onChanged(int position, int count);
/**
* Called when new items have been loaded at the end or beginning of the list.
*
* @param position Position of the first newly loaded item (in practice, either
* <code>0</code> or <code>size - 1</code>.
* @param count Number of items loaded.
*/
public abstract void onInserted(int position, int count);
/**
* Called when items have been removed at the end or beginning of the list, and have not
* been replaced by padded nulls.
*
* @param position Position of the first newly loaded item (in practice, either
* <code>0</code> or <code>size - 1</code>.
* @param count Number of items loaded.
*/
@SuppressWarnings("unused")
public abstract void onRemoved(int position, int count);
}
/**
* Configures how a PagedList loads content from its DataSource.
* <p>
* Use a Config {@link Builder} to construct and define custom loading behavior, such as
* {@link Builder#setPageSize(int)}, which defines number of items loaded at a time}.
*/
public static class Config {
/**
* Size of each page loaded by the PagedList.
*/
public final int pageSize;
/**
* Prefetch distance which defines how far ahead to load.
* <p>
* If this value is set to 50, the paged list will attempt to load 50 items in advance of
* data that's already been accessed.
*
* @see PagedList#loadAround(int)
*/
@SuppressWarnings("WeakerAccess")
public final int prefetchDistance;
/**
* Defines whether the PagedList may display null placeholders, if the DataSource provides
* them.
*/
@SuppressWarnings("WeakerAccess")
public final boolean enablePlaceholders;
/**
* Size hint for initial load of PagedList, often larger than a regular page.
*/
@SuppressWarnings("WeakerAccess")
public final int initialLoadSizeHint;
private Config(int pageSize, int prefetchDistance,
boolean enablePlaceholders, int initialLoadSizeHint) {
this.pageSize = pageSize;
this.prefetchDistance = prefetchDistance;
this.enablePlaceholders = enablePlaceholders;
this.initialLoadSizeHint = initialLoadSizeHint;
}
/**
* Builder class for {@link Config}.
* <p>
* You must at minimum specify page size with {@link #setPageSize(int)}.
*/
public static class Builder {
private int mPageSize = -1;
private int mPrefetchDistance = -1;
private int mInitialLoadSizeHint = -1;
private boolean mEnablePlaceholders = true;
/**
* Defines the number of items loaded at once from the DataSource.
* <p>
* Should be several times the number of visible items onscreen.
* <p>
* Configuring your page size depends on how your data is being loaded and used. Smaller
* page sizes improve memory usage, latency, and avoid GC churn. Larger pages generally
* improve loading throughput, to a point
* (avoid loading more than 2MB from SQLite at once, since it incurs extra cost).
* <p>
* If you're loading data for very large, social-media style cards that take up most of
* a screen, and your database isn't a bottleneck, 10-20 may make sense. If you're
* displaying dozens of items in a tiled grid, which can present items during a scroll
* much more quickly, consider closer to 100.
*
* @param pageSize Number of items loaded at once from the DataSource.
* @return this
*/
public Builder setPageSize(int pageSize) {
this.mPageSize = pageSize;
return this;
}
/**
* Defines how far from the edge of loaded content an access must be to trigger further
* loading.
* <p>
* Should be several times the number of visible items onscreen.
* <p>
* If not set, defaults to page size.
* <p>
* A value of 0 indicates that no list items will be loaded until they are specifically
* requested. This is generally not recommended, so that users don't observe a
* placeholder item (with placeholders) or end of list (without) while scrolling.
*
* @param prefetchDistance Distance the PagedList should prefetch.
* @return this
*/
public Builder setPrefetchDistance(int prefetchDistance) {
this.mPrefetchDistance = prefetchDistance;
return this;
}
/**
* Pass false to disable null placeholders in PagedLists using this Config.
* <p>
* If not set, defaults to true.
* <p>
* A PagedList will present null placeholders for not-yet-loaded content if two
* conditions are met:
* <p>
* 1) Its DataSource can count all unloaded items (so that the number of nulls to
* present is known).
* <p>
* 2) placeholders are not disabled on the Config.
* <p>
* Call {@code setEnablePlaceholders(false)} to ensure the receiver of the PagedList
* (often a {@link PagedListAdapter}) doesn't need to account for null items.
* <p>
* If placeholders are disabled, not-yet-loaded content will not be present in the list.
* Paging will still occur, but as items are loaded or removed, they will be signaled
* as inserts to the {@link PagedList.Callback}.
* {@link PagedList.Callback#onChanged(int, int)} will not be issued as part of loading,
* though a {@link PagedListAdapter} may still receive change events as a result of
* PagedList diffing.
*
* @param enablePlaceholders False if null placeholders should be disabled.
* @return this
*/
@SuppressWarnings("SameParameterValue")
public Builder setEnablePlaceholders(boolean enablePlaceholders) {
this.mEnablePlaceholders = enablePlaceholders;
return this;
}
/**
* Defines how many items to load when first load occurs, if you are using a
* {@link KeyedDataSource}.
* <p>
* This value is typically larger than page size, so on first load data there's a large
* enough range of content loaded to cover small scrolls.
* <p>
* If not set, defaults to three times page size.
*
* @param initialLoadSizeHint Number of items to load while initializing the PagedList.
* @return this
*/
@SuppressWarnings("WeakerAccess")
public Builder setInitialLoadSizeHint(int initialLoadSizeHint) {
this.mInitialLoadSizeHint = initialLoadSizeHint;
return this;
}
/**
* Creates a {@link Config} with the given parameters.
*
* @return A new Config.
*/
public Config build() {
if (mPageSize < 1) {
throw new IllegalArgumentException("Page size must be a positive number");
}
if (mPrefetchDistance < 0) {
mPrefetchDistance = mPageSize;
}
if (mInitialLoadSizeHint < 0) {
mInitialLoadSizeHint = mPageSize * 3;
}
if (!mEnablePlaceholders && mPrefetchDistance == 0) {
throw new IllegalArgumentException("Placeholders and prefetch are the only ways"
+ " to trigger loading of more data in the PagedList, so either"
+ " placeholders must be enabled, or prefetch distance must be > 0.");
}
return new Config(mPageSize, mPrefetchDistance,
mEnablePlaceholders, mInitialLoadSizeHint);
}
}
}
/**
* Signals when a PagedList has reached the end of available data.
* <p>
* This can be used to implement paging from the network into a local database - when the
* database has no more data to present, a BoundaryCallback can be used to fetch more data.
* <p>
* If an instance is shared across multiple PagedLists (e.g. when passed to
* {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple
* times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should
* avoid triggering it again while the load is ongoing.
*
* @param <T> Type loaded by the PagedList.
*/
@MainThread
public abstract static class BoundaryCallback<T> {
/**
* Called when zero items are returned from an initial load of the PagedList's data source.
*/
public void onZeroItemsLoaded() {}
/**
* Called when the item at the front of the PagedList has been loaded, and access has
* occurred within {@link Config#prefetchDistance} of it.
* <p>
* No more data will be prepended to the PagedList before this item.
*
* @param itemAtFront The first item of PagedList
*/
public void onItemAtFrontLoaded(@NonNull T itemAtFront) {}
/**
* Called when the item at the end of the PagedList has been loaded, and access has
* occurred within {@link Config#prefetchDistance} of it.
* <p>
* No more data will be appended to the PagedList after this item.
*
* @param itemAtEnd The first item of PagedList
*/
public void onItemAtEndLoaded(@NonNull T itemAtEnd) {}
}
}