java.lang.Object
↳java.util.AbstractCollection<java.lang.Object>
↳java.util.AbstractList<java.lang.Object>
↳androidx.paging.PagedList<T>
Overview
Lazy loading list that pages in immutable content from a DataSource.
A PagedList is a java.util.List
which loads its data in chunks (pages) from a DataSource.
Items can be accessed with PagedList.get(int), and further loading can be triggered with
PagedList.loadAround(int). To display a PagedList, see PagedListAdapter, which enables the
binding of a PagedList to a RecyclerView.
Loading Data
All data in a PagedList is loaded from its DataSource. Creating a PagedList loads the
first chunk of 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.
A PagedList initially presents this first partial load as its content, and expands over time as
content is loaded in. When PagedList.loadAround(int) is called, items will be loaded in near the passed
list index. If placeholder nulls 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.
PagedList can present data for an unbounded, infinite scrolling list, or a very large but
countable list. Use PagedList.Config to control how many items a PagedList loads, and when.
If you use LivePagedListBuilder to get a
LiveData<PagedList>, it will initialize PagedLists on a
background thread for you.
Placeholders
There are two ways that PagedList can represent its not-yet-loaded data - with or without
null placeholders.
With placeholders, the PagedList is always the full size of the data set. get(N) returns
the Nth item in the data set, or null if its not yet loaded.
Without 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 get(N)
returns the Nth loaded item. This is not necessarily the Nth item in the
data set.
Placeholders have several benefits:
- They express the full sized list to the presentation layer (often a
PagedListAdapter), and so can support scrollbars (without jumping as pages are
loaded or dropped) and fast-scrolling to any position, loaded or not.
- They avoid the need for a loading spinner at the end of the loaded list, since the list
is always full sized.
They also have drawbacks:
- Your Adapter needs to account for null items. This often means providing default
values in data you bind to a RecyclerView.ViewHolder.
- They don't work well if your item views are of different sizes, as this will prevent
loading items from cross-fading nicely.
- They require you to count your data set, which can be expensive or impossible, depending
on your DataSource.
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 false is passed to
PagedList.Config.Builder.setEnablePlaceholders(boolean) when building a PagedList.Config.
Mutability and Snapshots
A PagedList is
mutable while loading, or ready to load from its DataSource.
As loads succeed, a mutable PagedList will be updated via Runnables on the main thread. You can
listen to these updates with a
PagedList.Callback. (Note that
PagedListAdapter will listen
to these to signal RecyclerView about the updates/changes).
If a PagedList attempts to load from an invalid DataSource, it will PagedList.detach()
from the DataSource, meaning that it will no longer attempt to load data. It will return true
from PagedList.isImmutable(), and a new DataSource / PagedList pair must be created to load
further data. See DataSource and LivePagedListBuilder for how new PagedLists are
created to represent changed data.
A PagedList snapshot is simply an immutable shallow copy of the current state of the PagedList as
a List. It will reference the same inner items, and contain the same null
placeholders, if present.
Summary
Fields |
---|
from java.util.AbstractList<E> | modCount |
Methods |
---|
public void | addWeakCallback(java.util.List<java.lang.Object> previousSnapshot, PagedList.Callback callback)
Adds a callback, and issues updates since the previousSnapshot was created. |
public void | detach()
Detach the PagedList from its DataSource, and attempt to load no more data. |
public java.lang.Object | get(int index)
Get the item in the list of loaded items at the provided index. |
public PagedList.Config | getConfig()
Return the Config used to construct this PagedList. |
public abstract DataSource<java.lang.Object, java.lang.Object> | getDataSource()
Return the DataSource that provides data to this PagedList. |
public abstract java.lang.Object | getLastKey()
Return the key for the position passed most recently to PagedList.loadAround(int). |
public int | getLoadedCount()
Returns the number of items loaded in the PagedList. |
public int | getPositionOffset()
Position offset of the data in the list. |
public boolean | isDetached()
True if the PagedList has detached the DataSource it was loading from, and will no longer
load new data. |
public boolean | isImmutable()
Returns whether the list is immutable. |
public void | loadAround(int index)
Load adjacent items to passed index. |
public void | removeWeakCallback(PagedList.Callback callback)
Removes a previously added callback. |
public int | size()
Returns size of the list, including any not-yet-loaded null padding. |
public java.util.List<java.lang.Object> | snapshot()
Returns an immutable snapshot of the PagedList in its current state. |
from java.util.AbstractList<E> | add, add, addAll, clear, equals, hashCode, indexOf, iterator, lastIndexOf, listIterator, listIterator, remove, removeRange, set, subList |
from java.util.AbstractCollection<E> | addAll, contains, containsAll, isEmpty, remove, removeAll, retainAll, toArray, toArray, toString |
from java.lang.Object | clone, finalize, getClass, notify, notifyAll, wait, wait, wait |
Methods
public java.lang.Object
get(int index)
Get the item in the list of loaded items at the provided index.
Parameters:
index: Index in the loaded item list. Must be >= 0, and < PagedList.size()
Returns:
The item at the passed index, or null if a null placeholder is at the specified
position.
See also: PagedList.size()
public void
loadAround(int index)
Load adjacent items to passed index.
Parameters:
index: Index at which to load.
Returns size of the list, including any not-yet-loaded null padding.
To get the number of loaded items, not counting placeholders, use PagedList.getLoadedCount().
Returns:
Current total size of the list, including placeholders.
See also: PagedList.getLoadedCount()
public int
getLoadedCount()
Returns the number of items loaded in the PagedList.
Unlike PagedList.size() this counts only loaded items, not placeholders.
If placeholders are disabled, this method is equivalent to
PagedList.size().
Returns:
Number of items currently loaded, not counting placeholders.
See also: PagedList.size()
public boolean
isImmutable()
Returns whether the list is immutable.
Immutable lists may not become mutable again, and may safely be accessed from any thread.
In the future, this method may return true when a PagedList has completed loading from its
DataSource. Currently, it is equivalent to PagedList.isDetached().
Returns:
True if the PagedList is immutable.
public java.util.List<java.lang.Object>
snapshot()
Returns an immutable snapshot of the PagedList in its current state.
If this PagedList is immutable due to its DataSource being invalid, it
will be returned.
Returns:
Immutable snapshot of PagedList data.
Return the Config used to construct this PagedList.
Returns:
the Config of this PagedList
public abstract
DataSource<java.lang.Object, java.lang.Object>
getDataSource()
Return the DataSource that provides data to this PagedList.
Returns:
the DataSource of this PagedList.
public abstract java.lang.Object
getLastKey()
Return the key for the position passed most recently to PagedList.loadAround(int).
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 LivePagedListBuilder, it will do
this for you.
Returns:
Key of position most recently passed to PagedList.loadAround(int).
public boolean
isDetached()
True if the PagedList has detached the DataSource it was loading from, and will no longer
load new data.
A detached list is immutable.
Returns:
True if the data source is detached.
Detach the PagedList from its DataSource, and attempt to load no more data.
This is called automatically when a DataSource load returns null
, which is a
signal to stop loading. The PagedList will continue to present existing data, but will not
initiate new loads.
public int
getPositionOffset()
Position offset of the data in the list.
If data is supplied by a PositionalDataSource, the item returned from
get(i)
has a position of i + getPositionOffset()
.
If the DataSource is a ItemKeyedDataSource or PageKeyedDataSource, it
doesn't use positions, returns 0.
public void
addWeakCallback(java.util.List<java.lang.Object> previousSnapshot,
PagedList.Callback callback)
Adds a callback, and issues updates since the previousSnapshot was created.
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
onChanged(14, 2)
.
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.
The callback is internally held as weak reference, so PagedList doesn't hold a strong
reference to its observer, such as a 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.
Parameters:
previousSnapshot: Snapshot previously captured from this List, or null.
callback: Callback to dispatch to.
See also: PagedList.removeWeakCallback(PagedList.Callback)
Removes a previously added callback.
Parameters:
callback: Callback, previously added.
See also: PagedList.addWeakCallback(List, PagedList.Callback)
Source
/*
* Copyright 2018 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 androidx.paging;
import androidx.annotation.AnyThread;
import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.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 immutable 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)}. To display a PagedList, see {@link PagedListAdapter}, which enables the
* binding of a PagedList to a {@link androidx.recyclerview.widget.RecyclerView}.
* <h4>Loading Data</h4>
* <p>
* All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads the
* first chunk of 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>
* A PagedList initially presents this first partial load as its content, and expands over time as
* content is loaded in. 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 androidx.lifecycle.LiveData}<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 or dropped) and fast-scrolling to any position, 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 needs to account for {@code null} items. This often means providing default
* values in data you bind to a {@link androidx.recyclerview.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 your DataSource.
* </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}.
* <h4>Mutability and Snapshots</h4>
* A PagedList is <em>mutable</em> while loading, or ready to load from its DataSource.
* As loads succeed, a mutable PagedList will be updated via Runnables on the main thread. You can
* listen to these updates with a {@link Callback}. (Note that {@link PagedListAdapter} will listen
* to these to signal RecyclerView about the updates/changes).
* <p>
* If a PagedList attempts to load from an invalid DataSource, it will {@link #detach()}
* from the DataSource, meaning that it will no longer attempt to load data. It will return true
* from {@link #isImmutable()}, and a new DataSource / PagedList pair must be created to load
* further data. See {@link DataSource} and {@link LivePagedListBuilder} for how new PagedLists are
* created to represent changed data.
* <p>
* A PagedList snapshot is simply an immutable shallow copy of the current state of the PagedList as
* a {@code List}. It will reference the same inner items, and contain the same {@code null}
* placeholders, if present.
*
* @param <T> The type of the entries in the list.
*/
public abstract class PagedList<T> extends AbstractList<T> {
// Notes on threading:
//
// PagedList and its subclasses are passed and accessed on multiple threads, but are always
// owned by a single thread. During initialization, this is the creation thread, generally the
// fetchExecutor/fetchScheduler when using LiveData/RxJava. After initialization, the PagedList
// is owned by the main thread (or notifyScheduler, if overridden in RxJava).
//
// The only exception is detach()/isDetached(), which can be called from the fetch thread.
// However these methods simply wrap a atomic boolean, so are safe.
//
// The PageResult.Receiver that receives new data from the DataSource is always run on the
// owning thread.
@NonNull
final Executor mMainThreadExecutor;
@NonNull
final Executor mBackgroundThreadExecutor;
@Nullable
final BoundaryCallback<T> mBoundaryCallback;
@NonNull
final Config mConfig;
@NonNull
final PagedStorage<T> mStorage;
/**
* Last access location, in total position space (including offset).
* <p>
* Used by positional data
* sources to initialize loading near viewport
*/
int mLastLoad = 0;
T mLastItem = null;
final int mRequiredRemainder;
// if set to true, mBoundaryCallback is non-null, and should
// be dispatched when nearby load has occurred
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mBoundaryCallbackBeginDeferred = false;
@SuppressWarnings("WeakerAccess") /* synthetic access */
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);
private 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;
mRequiredRemainder = mConfig.prefetchDistance * 2 + mConfig.pageSize;
}
/**
* 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 notifyExecutor Thread that will use and consume data from the PagedList.
* Generally, this is the UI/main thread.
* @param fetchExecutor 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
@SuppressWarnings("WeakerAccess") /* synthetic access */
static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key) {
if (dataSource.isContiguous() || !config.enablePlaceholders) {
int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
if (!dataSource.isContiguous()) {
//noinspection unchecked
dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
.wrapAsContiguousWithoutPlaceholders();
if (key != null) {
lastLoad = (Integer) key;
}
}
ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
notifyExecutor,
fetchExecutor,
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 final class Builder<Key, Value> {
private final DataSource<Key, Value> mDataSource;
private final Config mConfig;
private Executor mNotifyExecutor;
private Executor mFetchExecutor;
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 page loading updates are dispatched.
*
* @param notifyExecutor Executor that receives PagedList updates, and where
* {@link Callback} calls are dispatched. Generally, this is the ui/main thread.
* @return this
*/
@NonNull
public Builder<Key, Value> setNotifyExecutor(@NonNull Executor notifyExecutor) {
mNotifyExecutor = notifyExecutor;
return this;
}
/**
* The executor used to fetch additional pages from the DataSource.
*
* Does not affect initial load, which will be done immediately on whichever thread the
* PagedList is created on.
*
* @param fetchExecutor Executor used to fetch from DataSources, generally a background
* thread pool for e.g. I/O or network loading.
* @return this
*/
@NonNull
public Builder<Key, Value> setFetchExecutor(@NonNull Executor fetchExecutor) {
mFetchExecutor = fetchExecutor;
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 (mNotifyExecutor == null) {
throw new IllegalArgumentException("MainThreadExecutor required");
}
if (mFetchExecutor == null) {
throw new IllegalArgumentException("BackgroundThreadExecutor required");
}
//noinspection unchecked
return PagedList.create(
mDataSource,
mNotifyExecutor,
mFetchExecutor,
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 < {@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) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
}
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("Can't defer BoundaryCallback, no instance");
}
/*
* 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.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
void tryDispatchBoundaryCallbacks(boolean post) {
final boolean dispatchBegin = mBoundaryCallbackBeginDeferred
&& mLowestIndexAccessed <= mConfig.prefetchDistance;
final boolean dispatchEnd = mBoundaryCallbackEndDeferred
&& mHighestIndexAccessed >= size() - 1 - 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);
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
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 offsetAccessIndices(int offset) {
// update last loadAround index
mLastLoad += offset;
// update access range
mLowestIndexAccessed += offset;
mHighestIndexAccessed += offset;
}
/**
* Returns size of the list, including any not-yet-loaded null padding.
*
* To get the number of loaded items, not counting placeholders, use {@link #getLoadedCount()}.
*
* @return Current total size of the list, including placeholders.
*
* @see #getLoadedCount()
*/
@Override
public int size() {
return mStorage.size();
}
/**
* Returns the number of items loaded in the PagedList.
*
* Unlike {@link #size()} this counts only loaded items, not placeholders.
* <p>
* If placeholders are {@link Config#enablePlaceholders disabled}, this method is equivalent to
* {@link #size()}.
*
* @return Number of items currently loaded, not counting placeholders.
*
* @see #size()
*/
public int getLoadedCount() {
return mStorage.getLoadedCount();
}
/**
* Returns whether the list is immutable.
*
* Immutable lists may not become mutable again, and may safely be accessed from any thread.
* <p>
* In the future, this method may return true when a PagedList has completed loading from its
* DataSource. Currently, it is equivalent to {@link #isDetached()}.
*
* @return True if the PagedList is immutable.
*/
@SuppressWarnings("WeakerAccess")
public boolean isImmutable() {
return isDetached();
}
/**
* Returns an immutable snapshot of the PagedList in its current state.
*
* If this PagedList {@link #isImmutable() is immutable} due to its DataSource being invalid, 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 DataSource that provides data to this PagedList.
*
* @return the DataSource of this PagedList.
*/
@NonNull
public abstract DataSource<?, T> getDataSource();
/**
* 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.
* <p>
* A detached list is {@link #isImmutable() immutable}.
*
* @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 ItemKeyedDataSource} or {@link PageKeyedDataSource}, it
* 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 Callback 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--) {
final 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 Callback, previously added.
* @see #addWeakCallback(List, Callback)
*/
@SuppressWarnings("WeakerAccess")
public void removeWeakCallback(@NonNull Callback callback) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
final 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--) {
final 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--) {
final Callback callback = mCallbacks.get(i).get();
if (callback != null) {
callback.onChanged(position, count);
}
}
}
}
void notifyRemoved(int position, int count) {
if (count != 0) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
final Callback callback = mCallbacks.get(i).get();
if (callback != null) {
callback.onRemoved(position, count);
}
}
}
}
/**
* Dispatch updates since the non-empty snapshot was taken.
*
* @param snapshot Non-empty snapshot.
* @param callback Callback 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#setNotifyExecutor(Executor)}, which is generally
* 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 {
/**
* When {@link #maxSize} is set to {@code MAX_SIZE_UNBOUNDED}, the maximum number of items
* loaded is unbounded, and pages will never be dropped.
*/
@SuppressWarnings("WeakerAccess")
public static final int MAX_SIZE_UNBOUNDED = Integer.MAX_VALUE;
/**
* 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;
/**
* Defines the maximum number of items that may be loaded into this pagedList before pages
* should be dropped.
* <p>
* {@link PageKeyedDataSource} does not currently support dropping pages - when
* loading from a {@code PageKeyedDataSource}, this value is ignored.
*
* @see #MAX_SIZE_UNBOUNDED
* @see Builder#setMaxSize(int)
*/
public final int maxSize;
/**
* Size hint for initial load of PagedList, often larger than a regular page.
*/
@SuppressWarnings("WeakerAccess")
public final int initialLoadSizeHint;
Config(int pageSize, int prefetchDistance,
boolean enablePlaceholders, int initialLoadSizeHint, int maxSize) {
this.pageSize = pageSize;
this.prefetchDistance = prefetchDistance;
this.enablePlaceholders = enablePlaceholders;
this.initialLoadSizeHint = initialLoadSizeHint;
this.maxSize = maxSize;
}
/**
* Builder class for {@link Config}.
* <p>
* You must at minimum specify page size with {@link #setPageSize(int)}.
*/
public static final class Builder {
static final int DEFAULT_INITIAL_PAGE_MULTIPLIER = 3;
private int mPageSize = -1;
private int mPrefetchDistance = -1;
private int mInitialLoadSizeHint = -1;
private boolean mEnablePlaceholders = true;
private int mMaxSize = MAX_SIZE_UNBOUNDED;
/**
* 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
*/
@NonNull
public Builder setPageSize(@IntRange(from = 1) int pageSize) {
if (pageSize < 1) {
throw new IllegalArgumentException("Page size must be a positive number");
}
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
*/
@NonNull
public Builder setPrefetchDistance(@IntRange(from = 0) int prefetchDistance) {
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")
@NonNull
public Builder setEnablePlaceholders(boolean enablePlaceholders) {
mEnablePlaceholders = enablePlaceholders;
return this;
}
/**
* Defines how many items to load when first load occurs.
* <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>
* When using a {@link PositionalDataSource}, the initial load size will be coerced to
* an integer multiple of pageSize, to enable efficient tiling.
* <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")
@NonNull
public Builder setInitialLoadSizeHint(@IntRange(from = 1) int initialLoadSizeHint) {
mInitialLoadSizeHint = initialLoadSizeHint;
return this;
}
/**
* Defines how many items to keep loaded at once.
* <p>
* This can be used to cap the number of items kept in memory by dropping pages. This
* value is typically many pages so old pages are cached in case the user scrolls back.
* <p>
* This value must be at least two times the
* {@link #setPrefetchDistance(int)} prefetch distance} plus the
* {@link #setPageSize(int) page size}). This constraint prevent loads from being
* continuously fetched and discarded due to prefetching.
* <p>
* The max size specified here best effort, not a guarantee. In practice, if maxSize
* is many times the page size, the number of items held by the PagedList will not grow
* above this number. Exceptions are made as necessary to guarantee:
* <ul>
* <li>Pages are never dropped until there are more than two pages loaded. Note that
* a DataSource may not be held strictly to
* {@link Config#pageSize requested pageSize}, so two pages may be larger than
* expected.
* <li>Pages are never dropped if they are within a prefetch window (defined to be
* {@code pageSize + (2 * prefetchDistance)}) of the most recent load.
* </ul>
* <p>
* {@link PageKeyedDataSource} does not currently support dropping pages - when
* loading from a {@code PageKeyedDataSource}, this value is ignored.
* <p>
* If not set, defaults to {@code MAX_SIZE_UNBOUNDED}, which disables page dropping.
*
* @param maxSize Maximum number of items to keep in memory, or
* {@code MAX_SIZE_UNBOUNDED} to disable page dropping.
* @return this
*
* @see Config#MAX_SIZE_UNBOUNDED
* @see Config#maxSize
*/
@NonNull
public Builder setMaxSize(@IntRange(from = 2) int maxSize) {
mMaxSize = maxSize;
return this;
}
/**
* Creates a {@link Config} with the given parameters.
*
* @return A new Config.
*/
@NonNull
public Config build() {
if (mPrefetchDistance < 0) {
mPrefetchDistance = mPageSize;
}
if (mInitialLoadSizeHint < 0) {
mInitialLoadSizeHint = mPageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER;
}
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.");
}
if (mMaxSize != MAX_SIZE_UNBOUNDED) {
if (mMaxSize < mPageSize + mPrefetchDistance * 2) {
throw new IllegalArgumentException("Maximum size must be at least"
+ " pageSize + 2*prefetchDist, pageSize=" + mPageSize
+ ", prefetchDist=" + mPrefetchDistance + ", maxSize=" + mMaxSize);
}
}
return new Config(mPageSize, mPrefetchDistance,
mEnablePlaceholders, mInitialLoadSizeHint, mMaxSize);
}
}
}
/**
* Signals when a PagedList has reached the end of available data.
* <p>
* When local storage is a cache of network data, it's common to set up a streaming pipeline:
* Network data is paged into the database, database is paged into UI. Paging from the database
* to UI can be done with a {@code LiveData<PagedList>}, but it's still necessary to know when
* to trigger network loads.
* <p>
* BoundaryCallback does this signaling - when a DataSource runs out of data at the end of
* the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network
* load that will write the result directly to the database. Because the database is being
* observed, the UI bound to the {@code LiveData<PagedList>} will update automatically to
* account for the new items.
* <p>
* Note that a BoundaryCallback instance 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.
* <p>
* The database + network Repository in the
* <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
* shows how to implement a network BoundaryCallback using
* <a href="https://square.github.io/retrofit/">Retrofit</a>, while
* handling swipe-to-refresh, network errors, and retry.
* <h4>Requesting Network Data</h4>
* BoundaryCallback only passes the item at front or end of the list when out of data. This
* makes it an easy fit for item-keyed network requests, where you can use the item passed to
* the BoundaryCallback to request more data from the network. In these cases, the source of
* truth for next page to load is coming from local storage, based on what's already loaded.
* <p>
* If you aren't using an item-keyed network API, you may be using page-keyed, or page-indexed.
* If this is the case, the paging library doesn't know about the page key or index used in the
* BoundaryCallback, so you need to track it yourself. You can do this in one of two ways:
* <h5>Local storage Page key</h5>
* If you want to perfectly resume your query, even if the app is killed and resumed, you can
* store the key on disk. Note that with a positional/page index network API, there's a simple
* way to do this, by using the {@code listSize} as an input to the next load (or
* {@code listSize / NETWORK_PAGE_SIZE}, for page indexing).
* <p>
* The current list size isn't passed to the BoundaryCallback though. This is because the
* PagedList doesn't necessarily know the number of items in local storage. Placeholders may be
* disabled, or the DataSource may not count total number of items.
* <p>
* Instead, for these positional cases, you can query the database for the number of items, and
* pass that to the network.
* <h5>In-Memory Page key</h5>
* Often it doesn't make sense to query the next page from network if the last page you fetched
* was loaded many hours or days before. If you keep the key in memory, you can refresh any time
* you start paging from a network source.
* <p>
* Store the next key in memory, inside your BoundaryCallback. When you create a new
* BoundaryCallback when creating a new {@code LiveData}/{@code Observable} of
* {@code PagedList}, refresh data. For example,
* <a href="https://codelabs.developers.google.com/codelabs/android-paging/index.html#8">in the
* Paging Codelab</a>, the GitHub network page index is stored in memory.
*
* @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) {}
}
}