diff options
Diffstat (limited to 'android/arch/paging')
-rw-r--r-- | android/arch/paging/BoundedDataSource.java | 4 | ||||
-rw-r--r-- | android/arch/paging/ContiguousDataSource.java | 8 | ||||
-rw-r--r-- | android/arch/paging/DataSource.java | 3 | ||||
-rw-r--r-- | android/arch/paging/KeyedDataSource.java | 67 | ||||
-rw-r--r-- | android/arch/paging/PagedList.java | 119 | ||||
-rw-r--r-- | android/arch/paging/PagedListAdapter.java | 39 | ||||
-rw-r--r-- | android/arch/paging/PagedListAdapterHelper.java | 11 | ||||
-rw-r--r-- | android/arch/paging/TestExecutor.java | 2 | ||||
-rw-r--r-- | android/arch/paging/TiledDataSource.java | 19 |
9 files changed, 219 insertions, 53 deletions
diff --git a/android/arch/paging/BoundedDataSource.java b/android/arch/paging/BoundedDataSource.java index 96c23fc5..664ab16c 100644 --- a/android/arch/paging/BoundedDataSource.java +++ b/android/arch/paging/BoundedDataSource.java @@ -18,6 +18,7 @@ package android.arch.paging; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; +import android.support.annotation.WorkerThread; import java.util.ArrayList; import java.util.Collections; @@ -49,15 +50,18 @@ public abstract class BoundedDataSource<Value> extends PositionalDataSource<Valu * @return List of loaded items. Null if the BoundedDataSource is no longer valid, and should * not be queried again. */ + @WorkerThread @Nullable public abstract List<Value> loadRange(int startPosition, int loadCount); + @WorkerThread @Nullable @Override public List<Value> loadAfter(int startIndex, int pageSize) { return loadRange(startIndex, pageSize); } + @WorkerThread @Nullable @Override public List<Value> loadBefore(int startIndex, int pageSize) { diff --git a/android/arch/paging/ContiguousDataSource.java b/android/arch/paging/ContiguousDataSource.java index 9ff11173..afcc208c 100644 --- a/android/arch/paging/ContiguousDataSource.java +++ b/android/arch/paging/ContiguousDataSource.java @@ -41,6 +41,8 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V return true; } + /** @hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable public abstract NullPaddedList<Value> loadInitial( @@ -58,7 +60,10 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V * @param pageSize Suggested number of items to load. * @return List of items, starting at position currentEndIndex + 1. Null if the data source is * no longer valid, and should not be queried again. + * + * @hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable public final List<Value> loadAfter(int currentEndIndex, @@ -88,7 +93,10 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V * on item contents. * @param pageSize Suggested number of items to load. * @return List of items, in descending order, starting at position currentBeginIndex - 1. + * + * @hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable public final List<Value> loadBefore(int currentBeginIndex, diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java index 1e815690..48fbec5f 100644 --- a/android/arch/paging/DataSource.java +++ b/android/arch/paging/DataSource.java @@ -17,6 +17,7 @@ package android.arch.paging; import android.support.annotation.AnyThread; +import android.support.annotation.WorkerThread; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -64,6 +65,7 @@ public abstract class DataSource<Key, Value> { * @return number of items that this DataSource can provide in total, or * {@link #COUNT_UNDEFINED} if expensive or undesired to compute. */ + @WorkerThread public abstract int countItems(); /** @@ -143,6 +145,7 @@ public abstract class DataSource<Key, Value> { * * @return True if the data source is invalid, and can no longer return data. */ + @WorkerThread public boolean isInvalid() { return mInvalid.get(); } diff --git a/android/arch/paging/KeyedDataSource.java b/android/arch/paging/KeyedDataSource.java index 057eb7f5..8cf6829c 100644 --- a/android/arch/paging/KeyedDataSource.java +++ b/android/arch/paging/KeyedDataSource.java @@ -16,6 +16,7 @@ package android.arch.paging; +import android.support.annotation.AnyThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; @@ -54,7 +55,7 @@ import java.util.List; * {@literal @}SuppressWarnings("FieldCanBeLocal") * private final InvalidationTracker.Observer mObserver; * - * public OffsetUserQueryDataSource(MyDatabase db) { + * public KeyedUserQueryDataSource(MyDatabase db) { * mDb = db; * mUserDao = db.getUserDao(); * mObserver = new InvalidationTracker.Observer("user") { @@ -85,11 +86,15 @@ import java.util.List; * * {@literal @}Override * public List<User> loadBefore({@literal @}NonNull String userName, int pageSize) { + * // Return items adjacent to 'userName' in reverse order + * // it's valid to return a different-sized list of items than pageSize, if it's easier * return mUserDao.userNameLoadBefore(userName, pageSize); * } * * {@literal @}Override * public List<User> loadAfter({@literal @}Nullable String userName, int pageSize) { + * // Return items adjacent to 'userName' + * // it's valid to return a different-sized list of items than pageSize, if it's easier * return mUserDao.userNameLoadAfter(userName, pageSize); * } * }</pre> @@ -198,13 +203,64 @@ public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<K return list; } + /** + * Return a key associated with the given item. + * <p> + * If your KeyedDataSource is loading from a source that is sorted and loaded by a unique + * integer ID, you would return {@code item.getID()} here. This key can then be passed to + * {@link #loadBefore(Key, int)} or {@link #loadAfter(Key, int)} to load additional items + * adjacent to the item passed to this function. + * <p> + * If your key is more complex, such as when you're sorting by name, then resolving collisions + * with integer ID, you'll need to return both. In such a case you would use a wrapper class, + * such as {@code Pair<String, Integer>} or, in Kotlin, + * {@code data class Key(val name: String, val id: Int)} + * + * @param item Item to get the key from. + * @return Key associated with given item. + */ @NonNull + @AnyThread public abstract Key getKey(@NonNull Value item); + /** + * Return the number of items that occur before the item uniquely identified by {@code key} in + * the data set. + * <p> + * For example, if you're loading items sorted by ID, then this would return the total number of + * items with ID less than {@code key}. + * <p> + * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsAfter(Key)}, your + * data source will not present placeholder null items in place of unloaded data. + * + * @param key A unique identifier of an item in the data set. + * @return Number of items in the data set before the item identified by {@code key}, or + * {@link #COUNT_UNDEFINED}. + * + * @see #countItemsAfter(Key) + */ + @WorkerThread public int countItemsBefore(@NonNull Key key) { return COUNT_UNDEFINED; } + /** + * Return the number of items that occur after the item uniquely identified by {@code key} in + * the data set. + * <p> + * For example, if you're loading items sorted by ID, then this would return the total number of + * items with ID greater than {@code key}. + * <p> + * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsBefore(Key)}, your + * data source will not present placeholder null items in place of unloaded data. + * + * @param key A unique identifier of an item in the data set. + * @return Number of items in the data set after the item identified by {@code key}, or + * {@link #COUNT_UNDEFINED}. + * + * @see #countItemsBefore(Key) + */ + @WorkerThread public int countItemsAfter(@NonNull Key key) { return COUNT_UNDEFINED; } @@ -231,10 +287,17 @@ public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<K public abstract List<Value> loadAfter(@NonNull Key currentEndKey, int pageSize); /** - * Load data before the currently loaded content, starting at the provided index. + * Load data before the currently loaded content, starting at the provided index, + * in reverse-display order. * <p> * It's valid to return a different list size than the page size, if it's easier for this data * source. It is generally safer to increase the number loaded than reduce. + * <p class="note"><strong>Note:</strong> Items returned from loadBefore <em>must</em> be in + * reverse order from how they will be presented in the list. The first item in the return list + * will be prepended immediately before the current beginning of the list. This is so that the + * KeyedDataSource may return a different number of items from the requested {@code pageSize} by + * shortening or lengthening the return list as it desires. + * <p> * * @param currentBeginKey Load items before this key. * @param pageSize Suggested number of items to load. diff --git a/android/arch/paging/PagedList.java b/android/arch/paging/PagedList.java index 0d5313dd..6a31b689 100644 --- a/android/arch/paging/PagedList.java +++ b/android/arch/paging/PagedList.java @@ -27,16 +27,65 @@ import java.util.concurrent.Executor; /** * Lazy loading list that pages in content from a {@link DataSource}. * <p> - * A PagedList is a lazy loaded list, which presents data from a {@link DataSource}. If the - * DataSource is counted (returns a valid number from its count method(s)), the PagedList will - * present {@code null} items in place of not-yet-loaded content to serve as placeholders. + * 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> - * When {@link #loadAround} is called, items will be loaded in near the passed position. If - * placeholder {@code null}s are present in the list, they will be replaced as content is loaded. + * 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> - * In this way, PagedList can present data for an unbounded, infinite scrolling list, or a very - * large but countable list. See {@link PagedListAdapter}, which enables the binding of a PagedList - * to a RecyclerView. Use {@link Config} to control how many items a PagedList loads, and when. + * 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 LivePagedListProvider} to get a + * {@link android.arch.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) 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 returns {@link DataSource#COUNT_UNDEFINED} from any item counting method, 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. */ @@ -92,6 +141,18 @@ public abstract class PagedList<T> extends AbstractList<T> { * Builder class for PagedList. * <p> * DataSource, main thread and background executor, and Config must all be provided. + * <p> + * A valid PagedList may not be constructed without data, so building a PagedList queries + * initial data from the data source. This is done because it's generally undesired to present a + * PagedList with no data in it to the UI. It's better 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> + * Because PagedLists are initialized with data, PagedLists must be built on a background + * thread. + * <p> + * {@link LivePagedListProvider} 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. @@ -174,11 +235,17 @@ public abstract class PagedList<T> extends AbstractList<T> { * <p> * This call will initial data and perform any counting needed to initialize the PagedList, * therefore it should only be called on a worker 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 */ - @NonNull @WorkerThread + @NonNull public PagedList<Value> build() { if (mDataSource == null) { throw new IllegalArgumentException("DataSource required"); @@ -403,6 +470,16 @@ public abstract class PagedList<T> extends AbstractList<T> { * 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 @@ -414,12 +491,15 @@ public abstract class PagedList<T> extends AbstractList<T> { /** * Defines how far from the edge of loaded content an access must be to trigger further - * loading. Defaults to page size. - * <p> - * A value of 0 indicates that no list items will be loaded before they are first - * requested. + * 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 @@ -432,8 +512,10 @@ public abstract class PagedList<T> extends AbstractList<T> { /** * Pass false to disable null placeholders in PagedLists using this Config. * <p> - * A PagedList will present null placeholders for not yet loaded content if two - * contitions are met: + * 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). @@ -442,6 +524,13 @@ public abstract class PagedList<T> extends AbstractList<T> { * <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 diff --git a/android/arch/paging/PagedListAdapter.java b/android/arch/paging/PagedListAdapter.java index 19a0c558..93c02ea3 100644 --- a/android/arch/paging/PagedListAdapter.java +++ b/android/arch/paging/PagedListAdapter.java @@ -51,6 +51,7 @@ import android.support.v7.widget.RecyclerView; * public final LiveData<PagedList<User>> usersList; * public MyViewModel(UserDao userDao) { * usersList = userDao.usersByLastName().create( + * /* initial load position {@literal *}/ 0, * new PagedList.Config.Builder() * .setPageSize(50) * .setPrefetchDistance(50) @@ -72,7 +73,7 @@ import android.support.v7.widget.RecyclerView; * * class UserAdapter extends PagedListAdapter<User, UserViewHolder> { * public UserAdapter() { - * super(User.DIFF_CALLBACK); + * super(DIFF_CALLBACK); * } * {@literal @}Override * public void onBindViewHolder(UserViewHolder holder, int position) { @@ -85,27 +86,21 @@ import android.support.v7.widget.RecyclerView; * holder.clear(); * } * } - * } - * - * {@literal @}Entity - * class User { - * // ... simple POJO code omitted ... - * - * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() { - * {@literal @}Override - * public boolean areItemsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // User properties may have changed if reloaded from the DB, but ID is fixed - * return oldUser.getId() == newUser.getId(); - * } - * {@literal @}Override - * public boolean areContentsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // NOTE: if you use equals, your object must properly override Object#equals() - * // Incorrectly returning false here will result in too many animations. - * return oldUser.equals(newUser); - * } - * } + * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() { + * {@literal @}Override + * public boolean areItemsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // User properties may have changed if reloaded from the DB, but ID is fixed + * return oldUser.getId() == newUser.getId(); + * } + * {@literal @}Override + * public boolean areContentsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // NOTE: if you use equals, your object must properly override Object#equals() + * // Incorrectly returning false here will result in too many animations. + * return oldUser.equals(newUser); + * } + * } * }</pre> * * Advanced users that wish for more control over adapter behavior, or to provide a specific base diff --git a/android/arch/paging/PagedListAdapterHelper.java b/android/arch/paging/PagedListAdapterHelper.java index 4bff5fcf..c7b61d9f 100644 --- a/android/arch/paging/PagedListAdapterHelper.java +++ b/android/arch/paging/PagedListAdapterHelper.java @@ -57,6 +57,7 @@ import java.util.List; * public final LiveData<PagedList<User>> usersList; * public MyViewModel(UserDao userDao) { * usersList = userDao.usersByLastName().create( + * /* initial load position {@literal *}/ 0, * new PagedList.Config.Builder() * .setPageSize(50) * .setPrefetchDistance(50) @@ -79,7 +80,7 @@ import java.util.List; * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { * private final PagedListAdapterHelper<User> mHelper; * public UserAdapter(PagedListAdapterHelper.Builder<User> builder) { - * mHelper = new PagedListAdapterHelper(this, User.DIFF_CALLBACK); + * mHelper = new PagedListAdapterHelper(this, DIFF_CALLBACK); * } * {@literal @}Override * public int getItemCount() { @@ -99,13 +100,7 @@ import java.util.List; * holder.clear(); * } * } - * } - * - * {@literal @}Entity - * class User { - * // ... simple POJO code omitted ... - * - * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() { + * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() { * {@literal @}Override * public boolean areItemsTheSame( * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { diff --git a/android/arch/paging/TestExecutor.java b/android/arch/paging/TestExecutor.java index 976f7df5..30809c3e 100644 --- a/android/arch/paging/TestExecutor.java +++ b/android/arch/paging/TestExecutor.java @@ -30,7 +30,7 @@ public class TestExecutor implements Executor { mTasks.add(command); } - public boolean executeAll() { + boolean executeAll() { boolean consumed = !mTasks.isEmpty(); Runnable task; while ((task = mTasks.poll()) != null) { diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java index 56556cd4..36be423d 100644 --- a/android/arch/paging/TiledDataSource.java +++ b/android/arch/paging/TiledDataSource.java @@ -17,6 +17,7 @@ package android.arch.paging; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; import java.util.List; @@ -90,6 +91,7 @@ public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> { * * @return Number of items this DataSource can provide. Must be <code>0</code> or greater. */ + @WorkerThread @Override public abstract int countItems(); @@ -100,14 +102,20 @@ public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> { /** * Called to load items at from the specified position range. + * <p> + * This method must return a list of requested size, unless at the end of list. Fixed size pages + * enable TiledDataSource to navigate tiles efficiently, and quickly accesss any position in the + * data set. + * <p> + * If a list of a different size is returned, but it is not the last list in the data set based + * on the return value from {@link #countItems()}, an exception will be thrown. * * @param startPosition Index of first item to load. - * @param count Exact number of items to load. Returning a different number will cause - * an exception to be thrown. - * @return List of loaded items. Null if the DataSource is no longer valid, and should - * not be queried again. + * @param count Number of items to load. + * @return List of loaded items, of the requested length unless at end of list. Null if the + * DataSource is no longer valid, and should not be queried again. */ - @SuppressWarnings("WeakerAccess") + @WorkerThread public abstract List<Type> loadRange(int startPosition, int count); final List<Type> loadRangeWrapper(int startPosition, int count) { @@ -132,6 +140,7 @@ public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> { mTiledDataSource = tiledDataSource; } + @WorkerThread @Nullable @Override public List<Value> loadRange(int startPosition, int loadCount) { |