Fix PageKeyedDataSource + BoundaryCallback - DO NOT MERGE
BoundaryCallback was not firing, because PageKeyedDataSource was
silently swallowing the end of data signal.
Fixes: 111187234
Test: ./gradlew paging:paging-common:test
Change-Id: Iaa9cadda6f631e820ae4ad9910aae3ba9b046607
diff --git a/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
index db6dddf..7c5445d 100644
--- a/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
@@ -337,6 +337,8 @@
if (key != null) {
loadAfter(new LoadParams<>(key, pageSize),
new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
+ } else {
+ receiver.onPageResult(PageResult.APPEND, PageResult.<Value>getEmptyResult());
}
}
@@ -348,6 +350,8 @@
if (key != null) {
loadBefore(new LoadParams<>(key, pageSize),
new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
+ } else {
+ receiver.onPageResult(PageResult.PREPEND, PageResult.<Value>getEmptyResult());
}
}
diff --git a/paging/common/src/main/java/androidx/paging/PageResult.java b/paging/common/src/main/java/androidx/paging/PageResult.java
index 7893176..e654a73 100644
--- a/paging/common/src/main/java/androidx/paging/PageResult.java
+++ b/paging/common/src/main/java/androidx/paging/PageResult.java
@@ -27,16 +27,29 @@
import java.util.List;
class PageResult<T> {
+ /**
+ * Single empty instance to avoid allocations.
+ * <p>
+ * Note, distinct from {@link #INVALID_RESULT} because {@link #isInvalid()} checks instance.
+ */
+ @SuppressWarnings("unchecked")
+ private static final PageResult EMPTY_RESULT =
+ new PageResult(Collections.emptyList(), 0);
+
@SuppressWarnings("unchecked")
private static final PageResult INVALID_RESULT =
new PageResult(Collections.emptyList(), 0);
@SuppressWarnings("unchecked")
+ static <T> PageResult<T> getEmptyResult() {
+ return EMPTY_RESULT;
+ }
+
+ @SuppressWarnings("unchecked")
static <T> PageResult<T> getInvalidResult() {
return INVALID_RESULT;
}
-
@Retention(SOURCE)
@IntDef({INIT, APPEND, PREPEND, TILE})
@interface ResultType {}
diff --git a/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
index 9f211fc..2010a15 100644
--- a/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
@@ -25,6 +25,7 @@
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
@RunWith(JUnit4::class)
class PageKeyedDataSourceTest {
@@ -41,8 +42,9 @@
private fun getPage(key: String): Page = data[key]!!
override fun loadInitial(
- params: LoadInitialParams<String>,
- callback: LoadInitialCallback<String, Item>) {
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String, Item>
+ ) {
val page = getPage(INIT_KEY)
callback.onResult(page.data, page.prev, page.next)
}
@@ -80,13 +82,16 @@
assertEquals(ITEM_LIST, pagedList)
}
- private fun performLoadInitial(invalidateDataSource: Boolean = false,
- callbackInvoker:
- (callback: PageKeyedDataSource.LoadInitialCallback<String, String>) -> Unit) {
+ private fun performLoadInitial(
+ invalidateDataSource: Boolean = false,
+ callbackInvoker:
+ (callback: PageKeyedDataSource.LoadInitialCallback<String, String>) -> Unit
+ ) {
val dataSource = object : PageKeyedDataSource<String, String>() {
override fun loadInitial(
- params: LoadInitialParams<String>,
- callback: LoadInitialCallback<String, String>) {
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String, String>
+ ) {
if (invalidateDataSource) {
// invalidate data source so it's invalid when onResult() called
invalidate()
@@ -95,14 +100,16 @@
}
override fun loadBefore(
- params: LoadParams<String>,
- callback: LoadCallback<String, String>) {
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>
+ ) {
fail("loadBefore not expected")
}
override fun loadAfter(
- params: LoadParams<String>,
- callback: LoadCallback<String, String>) {
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>
+ ) {
fail("loadAfter not expected")
}
}
@@ -159,6 +166,111 @@
it.onResult(emptyList(), 0, 1, null, null)
}
+ @Test
+ fun testBoundaryCallback() {
+ val dataSource = object : PageKeyedDataSource<String, String>() {
+ override fun loadInitial(
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String, String>
+ ) {
+ callback.onResult(listOf("B"), "a", "c")
+ }
+
+ override fun loadBefore(
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>
+ ) {
+ assertEquals("a", params.key)
+ callback.onResult(listOf("A"), null)
+ }
+
+ override fun loadAfter(
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>
+ ) {
+ assertEquals("c", params.key)
+ callback.onResult(listOf("C"), null)
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<String>
+ val executor = TestExecutor()
+ val pagedList = ContiguousPagedList<String, String>(
+ dataSource,
+ executor,
+ executor,
+ boundaryCallback,
+ PagedList.Config.Builder()
+ .setPageSize(10)
+ .build(),
+ "",
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ pagedList.loadAround(0)
+
+ verifyZeroInteractions(boundaryCallback)
+
+ executor.executeAll()
+
+ // verify boundary callbacks are triggered
+ verify(boundaryCallback).onItemAtFrontLoaded("A")
+ verify(boundaryCallback).onItemAtEndLoaded("C")
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ @Test
+ fun testBoundaryCallbackJustInitial() {
+ val dataSource = object : PageKeyedDataSource<String, String>() {
+ override fun loadInitial(
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String, String>
+ ) {
+ // just the one load, but boundary callbacks should still be triggered
+ callback.onResult(listOf("B"), null, null)
+ }
+
+ override fun loadBefore(
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>
+ ) {
+ fail("loadBefore not expected")
+ }
+
+ override fun loadAfter(
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>
+ ) {
+ fail("loadBefore not expected")
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<String>
+ val executor = TestExecutor()
+ val pagedList = ContiguousPagedList<String, String>(
+ dataSource,
+ executor,
+ executor,
+ boundaryCallback,
+ PagedList.Config.Builder()
+ .setPageSize(10)
+ .build(),
+ "",
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ pagedList.loadAround(0)
+
+ verifyZeroInteractions(boundaryCallback)
+
+ executor.executeAll()
+
+ // verify boundary callbacks are triggered
+ verify(boundaryCallback).onItemAtFrontLoaded("B")
+ verify(boundaryCallback).onItemAtEndLoaded("B")
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
private abstract class WrapperDataSource<K, A, B>(private val source: PageKeyedDataSource<K, A>)
: PageKeyedDataSource<K, B>() {
override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
@@ -177,11 +289,18 @@
return source.isInvalid
}
- override fun loadInitial(params: LoadInitialParams<K>,
- callback: LoadInitialCallback<K, B>) {
+ override fun loadInitial(
+ params: LoadInitialParams<K>,
+ callback: LoadInitialCallback<K, B>
+ ) {
source.loadInitial(params, object : LoadInitialCallback<K, A>() {
- override fun onResult(data: List<A>, position: Int, totalCount: Int,
- previousPageKey: K?, nextPageKey: K?) {
+ override fun onResult(
+ data: List<A>,
+ position: Int,
+ totalCount: Int,
+ previousPageKey: K?,
+ nextPageKey: K?
+ ) {
callback.onResult(convert(data), position, totalCount,
previousPageKey, nextPageKey)
}
@@ -218,8 +337,9 @@
}
}
- private fun verifyWrappedDataSource(createWrapper:
- (PageKeyedDataSource<String, Item>) -> PageKeyedDataSource<String, String>) {
+ private fun verifyWrappedDataSource(
+ createWrapper: (PageKeyedDataSource<String, Item>) -> PageKeyedDataSource<String, String>
+ ) {
// verify that it's possible to wrap a PageKeyedDataSource, and add info to its data
val orig = ItemDataSource(data = PAGE_MAP)
val wrapper = createWrapper(orig)