Improve docs, add PosDataSource.InitialLoadCallback#onResult(list, int)
Clarify several things in docs:
- Positional initial load when placeholders disabled
- Positional loadRange requires page sizes respected, others don't
- BoundaryCallback network + database usage
Test: Tests in paging-common
Test: ./gradlew createFlatfootDocsArchive -PofflineDocs=true
Change-Id: I8a3f5b286d349c71d1c35882a14098302a75498d
diff --git a/paging/common/src/main/java/android/arch/paging/DataSource.java b/paging/common/src/main/java/android/arch/paging/DataSource.java
index 635a910..23f8154 100644
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/DataSource.java
@@ -69,16 +69,19 @@
* {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
* <p>
* Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
- * example a network response that returns some items, and a next page and previous page links.
+ * example a network response that returns some items, and a next/previous page links.
* <p>
* Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
* {@code N}. For example, if requesting the backend for the next comments in the list
* requires the ID or timestamp of the most recent loaded comment, or if querying the next users
* from a name-sorted database query requires the name and unique ID of the previous.
* <p>
- * Use {@link PositionalDataSource} if you can load arbitrary pages based solely on position
- * information, and can provide a fixed item count. PositionalDataSource supports querying pages at
- * arbitrary positions, so can provide data to PagedLists in arbitrary order.
+ * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
+ * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
+ * PositionalDataSource is required to respect page size for efficient tiling. If you want to
+ * override page size (e.g. when network page size constraints are only known at runtime), use one
+ * of the other DataSource classes.
* <p>
* Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
* return {@code null} items in lists that it loads. This is so that users of the PagedList
@@ -114,8 +117,13 @@
/**
* Create a DataSource.
* <p>
- * The DataSource should invalidate itself if the snapshot is no longer valid, and a new
- * DataSource should be queried from the Factory.
+ * The DataSource should invalidate itself if the snapshot is no longer valid. If a
+ * DataSource becomes invalid, the only way to query more data is to create a new DataSource
+ * from the Factory.
+ * <p>
+ * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
+ * when the current DataSource is invalidated, and pass the new PagedList through the
+ * {@code LiveData<PagedList>} to observers.
*
* @return the new DataSource.
*/
diff --git a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
index 83278f8..5fb999b 100644
--- a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
@@ -108,6 +108,12 @@
* <p>
* A callback can be called only once, and will throw if called again.
* <p>
+ * If you can compute the number of items in the data set before and after the loaded range,
+ * call the three parameter {@link #onResult(List, int, int)} to pass that information. You
+ * can skip passing this information by calling the single parameter {@link #onResult(List)},
+ * either if it's difficult to compute, or if {@link LoadInitialParams#placeholdersEnabled} is
+ * {@code false}, so the positioning information will be ignored.
+ * <p>
* It is always valid for a DataSource loading method that takes a callback to stash the
* callback and call it later. This enables DataSources to be fully asynchronous, and to handle
* temporary, recoverable error states (such as a network error that can be retried).
diff --git a/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
index f406dee..5df12e5 100644
--- a/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
@@ -139,6 +139,13 @@
* <p>
* A callback can be called only once, and will throw if called again.
* <p>
+ * If you can compute the number of items in the data set before and after the loaded range,
+ * call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that
+ * information. You can skip passing this information by calling the three parameter
+ * {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if
+ * {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning
+ * information will be ignored.
+ * <p>
* It is always valid for a DataSource loading method that takes a callback to stash the
* callback and call it later. This enables DataSources to be fully asynchronous, and to handle
* temporary, recoverable error states (such as a network error that can be retried).
diff --git a/paging/common/src/main/java/android/arch/paging/PagedList.java b/paging/common/src/main/java/android/arch/paging/PagedList.java
index b6a95df..4bc9aa0 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedList.java
@@ -908,13 +908,26 @@
/**
* 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.
+ * 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>
- * If an instance is shared across multiple PagedLists (e.g. when passed to
+ * 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>
+ * BoundaryCallback only passes the item at front or end of the list. Number of items is not
+ * passed, since it may not be fully computed by the DataSource if placeholders are not
+ * supplied. Keys are not known because the BoundaryCallback is independent of the
+ * DataSource-specific keys, which may be different for local vs remote storage.
*
* @param <T> Type loaded by the PagedList.
*/
diff --git a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
index 819d56f..4b9f1c0 100644
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
@@ -25,15 +25,19 @@
import java.util.concurrent.Executor;
/**
- * Position-based data loader for a fixed-size, countable data set, supporting loads at arbitrary
- * positions.
+ * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
+ * arbitrary page positions.
* <p>
- * Extend PositionalDataSource if you can support counting your data set, and loading based on
- * position information.
+ * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. If your data source can't support loading arbitrary
+ * requested page sizes (e.g. when network page size constraints are only known at runtime), use
+ * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
* <p>
* Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
- * PositionalDataSource requires counting the size of the dataset. This allows pages to be tiled in
+ * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
* at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
+ * If placeholders are disabled, initialize with the two parameter
+ * {@link LoadInitialCallback#onResult(List, int)}.
* <p>
* Room can generate a Factory of PositionalDataSources for you:
* <pre>
@@ -148,7 +152,9 @@
* Call this method from your DataSource's {@code loadInitial} function to return data,
* and inform how many placeholders should be shown before and after. If counting is cheap
* to compute (for example, if a network load returns the information regardless), it's
- * recommended to pass data back through this method.
+ * recommended to pass the total size to the totalCount parameter. If placeholders are not
+ * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
+ * call {@link #onResult(List, int)}.
*
* @param data List of items loaded from the DataSource. If this is empty, the DataSource
* is treated as empty, and no further loads will occur.
@@ -179,11 +185,14 @@
}
/**
- * Called to pass initial load state from a DataSource without supporting placeholders.
+ * Called to pass initial load state from a DataSource without total count,
+ * when placeholders aren't requested.
+ * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
+ * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
* <p>
* Call this method from your DataSource's {@code loadInitial} function to return data,
- * if position is known but total size is not. If counting is not expensive, consider
- * calling the three parameter variant: {@link #onResult(List, int, int)}.
+ * if position is known but total size is not. If placeholders are requested, call the three
+ * parameter variant: {@link #onResult(List, int, int)}.
*
* @param data List of items loaded from the DataSource. If this is empty, the DataSource
* is treated as empty, and no further loads will occur.
@@ -191,10 +200,21 @@
* items before the items in data that can be provided by this DataSource,
* pass {@code N}.
*/
- void onResult(@NonNull List<T> data, int position) {
- // not counting, don't need to check mAcceptCount
- dispatchResultToReceiver(new PageResult<>(
- data, 0, 0, position));
+ @SuppressWarnings("WeakerAccess")
+ public void onResult(@NonNull List<T> data, int position) {
+ if (position < 0) {
+ throw new IllegalArgumentException("Position must be non-negative");
+ }
+ if (data.isEmpty() && position != 0) {
+ throw new IllegalArgumentException(
+ "Initial result cannot be empty if items are present in data set.");
+ }
+ if (mCountingEnabled) {
+ throw new IllegalStateException("Placeholders requested, but totalCount not"
+ + " provided. Please call the three-parameter onResult method, or disable"
+ + " placeholders in the PagedList.Config");
+ }
+ dispatchResultToReceiver(new PageResult<>(data, position));
}
}
@@ -237,7 +257,7 @@
new LoadInitialCallback<>(this, acceptCount, pageSize, receiver);
LoadInitialParams params = new LoadInitialParams(
- requestedStartPosition, requestedLoadSize, pageSize, true);
+ requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
loadInitial(params, callback);
// If initialLoad's callback is not called within the body, we force any following calls
diff --git a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
index da4b265..280a64d 100644
--- a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
@@ -80,6 +80,7 @@
}
private fun performInitialLoad(
+ enablePlaceholders: Boolean = true,
callbackInvoker: (callback: PositionalDataSource.LoadInitialCallback<String>) -> Unit) {
val dataSource = object : PositionalDataSource<String>() {
override fun loadInitial(
@@ -93,12 +94,16 @@
}
}
- TiledPagedList(
- dataSource, FailExecutor(), FailExecutor(), null,
- PagedList.Config.Builder()
- .setPageSize(10)
- .build(),
- 0)
+ val config = PagedList.Config.Builder()
+ .setPageSize(10)
+ .setEnablePlaceholders(enablePlaceholders)
+ .build()
+ if (enablePlaceholders) {
+ TiledPagedList(dataSource, FailExecutor(), FailExecutor(), null, config, 0)
+ } else {
+ ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
+ FailExecutor(), FailExecutor(), null, config, null)
+ }
}
@Test
@@ -137,4 +142,28 @@
// LoadInitialCallback can't accept empty result unless data set is empty
it.onResult(emptyList(), 0, 2)
}
+
+ @Test(expected = IllegalStateException::class)
+ fun initialLoadCallbackRequireTotalCount() = performInitialLoad(enablePlaceholders = true) {
+ // LoadInitialCallback requires 3 args when placeholders enabled
+ it.onResult(listOf("a", "b"), 0)
+ }
+
+ @Test
+ fun initialLoadCallbackSuccessTwoArg() = performInitialLoad(enablePlaceholders = false) {
+ // LoadInitialCallback correct 2 arg usage
+ it.onResult(listOf("a", "b"), 0)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackPosNegativeTwoArg() = performInitialLoad(enablePlaceholders = false) {
+ // LoadInitialCallback can't accept negative position
+ it.onResult(listOf("a", "b"), -1)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackEmptyWithOffset() = performInitialLoad(enablePlaceholders = false) {
+ // LoadInitialCallback can't accept empty result unless pos is 0
+ it.onResult(emptyList(), 1)
+ }
}