Merge "Initial support for inserting separators between paginated items" into androidx-master-dev
diff --git a/paging/common/api/3.0.0-alpha01.txt b/paging/common/api/3.0.0-alpha01.txt
index 8378693..6474103 100644
--- a/paging/common/api/3.0.0-alpha01.txt
+++ b/paging/common/api/3.0.0-alpha01.txt
@@ -95,6 +95,10 @@
enum_constant public static final androidx.paging.LoadType START;
}
+ public final class PageEventKt {
+ ctor public PageEventKt();
+ }
+
public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor public PageKeyedDataSource();
method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
@@ -312,6 +316,10 @@
field public final int startPosition;
}
+ public final class SeparatorsKt {
+ ctor public SeparatorsKt();
+ }
+
}
package androidx.paging.futures {
diff --git a/paging/common/api/current.txt b/paging/common/api/current.txt
index 8378693..6474103 100644
--- a/paging/common/api/current.txt
+++ b/paging/common/api/current.txt
@@ -95,6 +95,10 @@
enum_constant public static final androidx.paging.LoadType START;
}
+ public final class PageEventKt {
+ ctor public PageEventKt();
+ }
+
public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor public PageKeyedDataSource();
method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
@@ -312,6 +316,10 @@
field public final int startPosition;
}
+ public final class SeparatorsKt {
+ ctor public SeparatorsKt();
+ }
+
}
package androidx.paging.futures {
diff --git a/paging/common/api/public_plus_experimental_3.0.0-alpha01.txt b/paging/common/api/public_plus_experimental_3.0.0-alpha01.txt
index 8378693..6474103 100644
--- a/paging/common/api/public_plus_experimental_3.0.0-alpha01.txt
+++ b/paging/common/api/public_plus_experimental_3.0.0-alpha01.txt
@@ -95,6 +95,10 @@
enum_constant public static final androidx.paging.LoadType START;
}
+ public final class PageEventKt {
+ ctor public PageEventKt();
+ }
+
public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor public PageKeyedDataSource();
method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
@@ -312,6 +316,10 @@
field public final int startPosition;
}
+ public final class SeparatorsKt {
+ ctor public SeparatorsKt();
+ }
+
}
package androidx.paging.futures {
diff --git a/paging/common/api/public_plus_experimental_current.txt b/paging/common/api/public_plus_experimental_current.txt
index 8378693..6474103 100644
--- a/paging/common/api/public_plus_experimental_current.txt
+++ b/paging/common/api/public_plus_experimental_current.txt
@@ -95,6 +95,10 @@
enum_constant public static final androidx.paging.LoadType START;
}
+ public final class PageEventKt {
+ ctor public PageEventKt();
+ }
+
public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor public PageKeyedDataSource();
method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
@@ -312,6 +316,10 @@
field public final int startPosition;
}
+ public final class SeparatorsKt {
+ ctor public SeparatorsKt();
+ }
+
}
package androidx.paging.futures {
diff --git a/paging/common/api/restricted_3.0.0-alpha01.txt b/paging/common/api/restricted_3.0.0-alpha01.txt
index 310fe1f..ef0b418 100644
--- a/paging/common/api/restricted_3.0.0-alpha01.txt
+++ b/paging/common/api/restricted_3.0.0-alpha01.txt
@@ -113,6 +113,10 @@
property public abstract int trailingNullCount;
}
+ public final class PageEventKt {
+ ctor public PageEventKt();
+ }
+
public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor public PageKeyedDataSource();
method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
@@ -354,6 +358,10 @@
field public final int startPosition;
}
+ public final class SeparatorsKt {
+ ctor public SeparatorsKt();
+ }
+
}
package androidx.paging.futures {
diff --git a/paging/common/api/restricted_current.txt b/paging/common/api/restricted_current.txt
index 310fe1f..ef0b418 100644
--- a/paging/common/api/restricted_current.txt
+++ b/paging/common/api/restricted_current.txt
@@ -113,6 +113,10 @@
property public abstract int trailingNullCount;
}
+ public final class PageEventKt {
+ ctor public PageEventKt();
+ }
+
public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor public PageKeyedDataSource();
method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
@@ -354,6 +358,10 @@
field public final int startPosition;
}
+ public final class SeparatorsKt {
+ ctor public SeparatorsKt();
+ }
+
}
package androidx.paging.futures {
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt b/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
index 74ae229..8d74e6b 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
@@ -19,6 +19,8 @@
import androidx.paging.LoadType.END
import androidx.paging.LoadType.REFRESH
import androidx.paging.LoadType.START
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
/**
* Events in the stream from paging fetch logic to UI.
@@ -41,25 +43,29 @@
}
}
- private inline fun <R : Any> mapInternal(
+ private inline fun <R : Any> mapPages(
predicate: (TransformablePage<T>) -> TransformablePage<R>
+ ) = transformPages { it.map(predicate) }
+
+ internal inline fun <R : Any> transformPages(
+ predicate: (List<TransformablePage<T>>) -> List<TransformablePage<R>>
): PageEvent<R> = Insert(
loadType = loadType,
- pages = pages.map(predicate),
+ pages = predicate(pages),
placeholdersStart = placeholdersStart,
placeholdersEnd = placeholdersEnd
)
- override fun <R : Any> map(predicate: (T) -> R): PageEvent<R> = mapInternal {
+ override fun <R : Any> map(predicate: (T) -> R): PageEvent<R> = mapPages {
TransformablePage(
originalPageOffset = it.originalPageOffset,
data = it.data.map(predicate),
- sourcePageSize = it.sourcePageSize,
+ originalPageSize = it.originalPageSize,
originalIndices = it.originalIndices
)
}
- override fun <R : Any> flatMap(transform: (T) -> Iterable<R>): PageEvent<R> = mapInternal {
+ override fun <R : Any> flatMap(transform: (T) -> Iterable<R>): PageEvent<R> = mapPages {
val data = mutableListOf<R>()
val originalIndices = mutableListOf<Int>()
it.data.forEachIndexed { index, t ->
@@ -72,12 +78,12 @@
TransformablePage(
originalPageOffset = it.originalPageOffset,
data = data,
- sourcePageSize = it.sourcePageSize,
+ originalPageSize = it.originalPageSize,
originalIndices = originalIndices
)
}
- override fun filter(predicate: (T) -> Boolean): PageEvent<T> = mapInternal {
+ override fun filter(predicate: (T) -> Boolean): PageEvent<T> = mapPages {
val data = mutableListOf<T>()
val originalIndices = mutableListOf<Int>()
it.data.forEachIndexed { index, t ->
@@ -89,11 +95,26 @@
TransformablePage(
originalPageOffset = it.originalPageOffset,
data = data,
- sourcePageSize = it.sourcePageSize,
+ originalPageSize = it.originalPageSize,
originalIndices = originalIndices
)
}
+ override fun filterOutEmptyPages(
+ currentPages: MutableList<TransformablePage<T>>
+ ): PageEvent<T> {
+ // insert pre-filtered pages into list, so drop
+ // can account for pages we've filtered out here
+ applyToList(currentPages)
+
+ return if (pages.any { it.data.isEmpty() }) {
+ transformPages { pages -> pages.filter { it.data.isNotEmpty() } }
+ } else {
+ // no empty pages, can safely reuse this page
+ this
+ }
+ }
+
companion object {
fun <T : Any> Refresh(
pages: List<TransformablePage<T>>,
@@ -118,6 +139,7 @@
val count: Int,
val placeholdersRemaining: Int
) : PageEvent<T>() {
+
init {
require(loadType != REFRESH) { "Drop must be START or END" }
require(count >= 0) { "Invalid count $count" }
@@ -125,6 +147,46 @@
"Invalid placeholdersRemaining $placeholdersRemaining"
}
}
+
+ /**
+ * Alter the drop event to skip dropping any empty pages, since they won't have been
+ * sent downstream.
+ */
+ override fun filterOutEmptyPages(
+ currentPages: MutableList<TransformablePage<T>>
+ ): PageEvent<T> {
+ // decrease count by number of empty pages that would have been dropped, since these
+ // haven't been sent downstream
+ var newCount = count
+ if (loadType == START) {
+ repeat(count) { i ->
+ if (currentPages[i].data.isEmpty()) {
+ newCount--
+ }
+ }
+ } else {
+ repeat(count) { i ->
+ if (currentPages[currentPages.size - i].data.isEmpty()) {
+ newCount--
+ }
+ }
+ }
+
+ // apply drop to currentPages after newCount is computed, so it represents loaded
+ // pages before this tranform is applied
+ applyToList(currentPages)
+
+ return if (newCount == count) {
+ // no empty pages encountered
+ this
+ } else {
+ Drop(
+ loadType,
+ newCount,
+ placeholdersRemaining
+ )
+ }
+ }
}
data class StateUpdate<T : Any>(
@@ -139,4 +201,69 @@
open fun <R : Any> flatMap(transform: (T) -> Iterable<R>): PageEvent<R> = this as PageEvent<R>
open fun filter(predicate: (T) -> Boolean): PageEvent<T> = this
+
+ open fun filterOutEmptyPages(
+ currentPages: MutableList<TransformablePage<T>>
+ ): PageEvent<T> = this
+}
+
+/**
+ * TODO: optimize this per usecase, to avoid holding onto the whole page in memory
+ */
+internal fun <T : Any> PageEvent.Insert<T>.applyToList(
+ currentPages: MutableList<TransformablePage<T>>
+) {
+ when (loadType) {
+ REFRESH -> {
+ check(currentPages.isEmpty())
+ currentPages.addAll(pages)
+ }
+ START -> {
+ currentPages.addAll(0, pages)
+ }
+ END -> {
+ currentPages.addAll(currentPages.size, pages)
+ }
+ }
+}
+
+/**
+ * TODO: optimize this per usecase, to avoid holding onto the whole page in memory
+ */
+internal fun <T : Any> PageEvent.Drop<T>.applyToList(
+ currentPages: MutableList<TransformablePage<T>>
+) {
+ if (loadType == START) {
+ repeat(count) { currentPages.removeAt(0) }
+ } else {
+ repeat(count) { currentPages.removeAt(currentPages.lastIndex) }
+ }
+}
+
+/**
+ * Transforms the Flow to an output-equivalent Flow, which does not have empty pages.
+ *
+ * This can be used before accessing adjacent pages, to ensure adjacent pages have context in
+ * them.
+ */
+internal fun <T : Any> Flow<PageEvent<T>>.removeEmptyPages(): Flow<PageEvent<T>> {
+ val pages = mutableListOf<TransformablePage<T>>()
+
+ // TODO: consider dropping, or not even creating noop (empty) events entirely
+ return map { it.filterOutEmptyPages(pages) }
+}
+
+/**
+ * Transforms the Flow to include optional separators in between each pair of items in the output
+ * stream.
+ *
+ * TODO: support separator at beginning / end - requires tracking of loading state
+ * (to know when an Insert.Start event is terminal)
+ */
+internal fun <R : Any, T : R> Flow<PageEvent<T>>.insertSeparators(
+ predicate: (T?, T?) -> R?
+): Flow<PageEvent<R>> {
+ val pages = mutableListOf<TransformablePage<T>>()
+ return removeEmptyPages()
+ .map { event -> event.insertSeparators(pages, predicate) }
}
\ No newline at end of file
diff --git a/paging/common/src/main/kotlin/androidx/paging/Separators.kt b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
new file mode 100644
index 0000000..6ba26032
--- /dev/null
+++ b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2019 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.paging.LoadType.END
+import androidx.paging.LoadType.START
+import androidx.paging.PageEvent.Drop
+import androidx.paging.PageEvent.Insert
+import androidx.paging.PageEvent.StateUpdate
+
+/**
+ * Create a TransformablePage with separators inside (ignoring edges)
+ *
+ * Separators between pages are handled outside of the page, see [PageEvent.insertSeparators].
+ */
+private fun <R : Any, T : R> TransformablePage<T>.insertSeparators(
+ predicate: (T?, T?) -> R?
+): TransformablePage<R> {
+ if (data.isEmpty()) {
+ @Suppress("UNCHECKED_CAST")
+ return this as TransformablePage<R>
+ }
+
+ val initialCapacity = data.size + 4 // extra space to avoid bigger allocations
+ val outputList = ArrayList<R>(initialCapacity)
+ val outputIndices = ArrayList<Int>(initialCapacity)
+
+ outputList.add(data.first())
+ outputIndices.add(originalIndices?.first() ?: 0)
+ for (i in 1 until data.size) {
+ val item = data[i]
+ val separator = predicate(data[i - 1], item)
+ if (separator != null) {
+ outputList.add(separator)
+ outputIndices.add(i)
+ }
+ outputList.add(item)
+ outputIndices.add(i)
+ }
+ return TransformablePage(
+ originalPageOffset = originalPageOffset,
+ data = outputList,
+ originalPageSize = originalPageSize,
+ originalIndices = outputIndices
+ )
+}
+
+/**
+ * Create a TransformablePage with the given separator (or empty, if the separator is null)
+ *
+ * We create an empty page when a separator is not needed in order to simplify dropping. By
+ * ensuring there are always 2N-1 pages in the output event stream, every drop of a M pages in the
+ * input event stream can be simply transformed to a drop of 2 * M pages.
+ *
+ * TODO: consider tracking the separator pages differently, so we don't have to
+ * allocate these empty pages.
+ */
+private fun <R : Any, T : R> separatorPage(
+ adjacentPage: TransformablePage<T>,
+ separator: R?,
+ originalIndex: Int
+): TransformablePage<R> = if (separator != null) {
+ // page with just the separator
+ TransformablePage(
+ originalPageOffset = adjacentPage.originalPageOffset,
+ data = listOf(separator),
+ originalPageSize = adjacentPage.originalPageSize,
+ originalIndices = listOf(originalIndex)
+ )
+} else {
+ // empty page
+ TransformablePage(
+ originalPageOffset = adjacentPage.originalPageOffset,
+ data = emptyList(),
+ originalPageSize = adjacentPage.originalPageSize,
+ originalIndices = null
+ )
+}
+
+internal fun <R : Any, T : R> List<TransformablePage<T>>.insertSeparators(
+ loadType: LoadType,
+ itemAtStart: T?,
+ itemAtEnd: T?,
+ predicate: (T?, T?) -> R?
+): List<TransformablePage<R>> {
+ if (isEmpty()) {
+ return emptyList()
+ }
+
+ val outList = ArrayList<TransformablePage<R>>(size)
+
+ var itemBefore = itemAtStart
+ forEachIndexed { index, page ->
+ // If page is being appended, or if we're in between pages, insert separator page
+ if (index != 0 || loadType == END) {
+ val separator = predicate(itemBefore, page.data.first())
+ outList.add(separatorPage(page, separator, originalIndex = 0))
+ }
+
+ outList.add(page.insertSeparators(predicate))
+
+ itemBefore = page.data.last()
+ }
+
+ if (loadType == START) {
+ val lastPage = last()
+ val separator = predicate(lastPage.data.last(), itemAtEnd)
+ outList.add(
+ separatorPage(lastPage, separator, originalIndex = lastPage.originalPageSize - 1)
+ )
+ }
+
+ return outList
+}
+
+/**
+ * State-tracking operation on PageEvent to insert separators
+ *
+ * State is tracked in the mutable currentPages list, shared by all events in stream
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun <R : Any, T : R> PageEvent<T>.insertSeparators(
+ currentPages: MutableList<TransformablePage<T>>,
+ predicate: (T?, T?) -> R?
+): PageEvent<R> = when (this) {
+ is Insert<T> -> {
+ val newEvent = transformPages {
+ it.insertSeparators(
+ loadType = loadType,
+ itemAtStart = if (loadType == END) currentPages.last().data.last() else null,
+ itemAtEnd = if (loadType == START) currentPages.first().data.first() else null,
+ predicate = predicate
+ )
+ }
+
+ this.applyToList(currentPages)
+ newEvent
+ }
+ is Drop -> {
+ this.applyToList(currentPages)
+ Drop(
+ loadType = loadType,
+ count = count * 2,
+ placeholdersRemaining = placeholdersRemaining
+ )
+ }
+ is StateUpdate -> this as PageEvent<R>
+}
\ No newline at end of file
diff --git a/paging/common/src/main/kotlin/androidx/paging/TransformablePage.kt b/paging/common/src/main/kotlin/androidx/paging/TransformablePage.kt
index f7384dd..9889ddc 100644
--- a/paging/common/src/main/kotlin/androidx/paging/TransformablePage.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/TransformablePage.kt
@@ -30,7 +30,7 @@
/**
* Size of the original page (pre-transformation)
*/
- val sourcePageSize: Int,
+ val originalPageSize: Int,
/**
* Optional lookup table for page indices.
@@ -58,10 +58,10 @@
fun getLoadHint(relativeIndex: Int): ViewportHint {
val indexInPage = when {
relativeIndex < 0 -> relativeIndex
- relativeIndex >= data.size -> relativeIndex - data.size + sourcePageSize
+ relativeIndex >= data.size -> relativeIndex - data.size + originalPageSize
originalIndices != null -> originalIndices[relativeIndex]
else -> relativeIndex
}
return ViewportHint(originalPageOffset, indexInPage)
}
-}
\ No newline at end of file
+}
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageEventTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageEventTest.kt
index c4938a0..e68cce7e 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageEventTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageEventTest.kt
@@ -25,12 +25,6 @@
import kotlin.test.assertFailsWith
import kotlin.test.assertSame
-@Suppress("TestFunctionName")
-private fun <T : Any> TransformablePage(data: List<T>) = TransformablePage(
- data = data,
- originalPageOffset = 0
-)
-
@RunWith(JUnit4::class)
class PageEventTest {
@Test
@@ -136,7 +130,7 @@
pages = listOf(TransformablePage(
originalPageOffset = 0,
data = listOf("a", "b"),
- sourcePageSize = 4,
+ originalPageSize = 4,
originalIndices = listOf(0, 2)
)),
placeholdersEnd = 4
@@ -145,7 +139,7 @@
pages = listOf(TransformablePage(
originalPageOffset = 0,
data = listOf('a', 'b'),
- sourcePageSize = 4,
+ originalPageSize = 4,
originalIndices = listOf(0, 2)
)),
placeholdersEnd = 4
@@ -168,7 +162,7 @@
TransformablePage(
originalPageOffset = 0,
data = listOf('a', 'b', 'd'),
- sourcePageSize = 4,
+ originalPageSize = 4,
originalIndices = listOf(0, 1, 3)
)
),
@@ -184,7 +178,7 @@
TransformablePage(
originalPageOffset = 0,
data = listOf('b', 'd'),
- sourcePageSize = 4,
+ originalPageSize = 4,
originalIndices = listOf(1, 3)
)
),
@@ -211,7 +205,7 @@
TransformablePage(
originalPageOffset = 0,
data = listOf("a1", "a2", "b1", "b2"),
- sourcePageSize = 2,
+ originalPageSize = 2,
originalIndices = listOf(0, 0, 1, 1)
)
),
@@ -230,7 +224,7 @@
TransformablePage(
originalPageOffset = 0,
data = listOf("a1", "-", "a2", "-", "b1", "-", "b2", "-"),
- sourcePageSize = 2,
+ originalPageSize = 2,
originalIndices = listOf(0, 0, 0, 0, 1, 1, 1, 1)
)
),
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagerTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagerTest.kt
index ceb9191..4d21bff 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagerTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagerTest.kt
@@ -56,7 +56,7 @@
TransformablePage(
originalPageOffset = pageOffset,
data = items.slice(range),
- sourcePageSize = range.count(),
+ originalPageSize = range.count(),
originalIndices = null
)
)
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagingStateTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagingStateTest.kt
index 63b01c1e..82a4231 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingStateTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingStateTest.kt
@@ -30,7 +30,7 @@
indexOfInitialPage: Int = 0
) = processEvent(
PageEvent.Insert.Refresh(
- pages = listOfTransformablePages(pages, indexOfInitialPage),
+ pages = pages.toTransformablePages(indexOfInitialPage),
placeholdersStart = placeholdersStart,
placeholdersEnd = placeholdersEnd
)
@@ -77,17 +77,6 @@
placeholdersRemaining = placeholdersRemaining
)
)
-
-private fun <T : Any> listOfTransformablePages(
- pages: List<List<T>>,
- indexOfInitialPage: Int = 0
-) = pages.mapIndexed { index, list ->
- TransformablePage(
- data = list,
- originalPageOffset = index - indexOfInitialPage
- )
-}
-
@Suppress("TestFunctionName")
internal fun <T : Any> PagingState(
pages: List<List<T>>,
@@ -101,7 +90,7 @@
) = PagingState(
leadingNullCount = placeholdersStart,
trailingNullCount = placeholdersEnd,
- pages = listOfTransformablePages(pages, indexOfInitialPage),
+ pages = pages.toTransformablePages(indexOfInitialPage),
loadStateRefresh = refresh,
loadStateStart = start,
loadStateEnd = end,
diff --git a/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
new file mode 100644
index 0000000..eb15e5e
--- /dev/null
+++ b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2019 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.paging.PageEvent.Drop
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+
+private fun <T : Any> assertEvent(expected: PageEvent<T>, actual: PageEvent<T>) {
+ try {
+ assertEquals(expected, actual)
+ } catch (e: Throwable) {
+ throw AssertionError(
+ e.localizedMessage
+ .replace("),", "),\n")
+ .replace("<[", "<[\n ")
+ .replace("actual", "\nactual\n")
+ .replace("Expected", "\nExpected\n")
+ .replace("pages=", "pages=\n")
+ )
+ }
+}
+
+@RunWith(JUnit4::class)
+class SeparatorsTest {
+ @Test
+ fun separatorDrop() {
+ val initialState =
+ listOf('a', 'b', 'c', 'd')
+ .map { listOf(it) }
+ .toTransformablePages()
+ val outDrop = Drop<Char>(
+ loadType = LoadType.END,
+ count = 2,
+ placeholdersRemaining = 4
+ ).insertSeparators(
+ currentPages = initialState
+ ) { _, _ -> null }
+
+ // drop count always simply doubles, because each N pages, after separators, become 2N - 1
+ assertEvent(
+ Drop(
+ loadType = LoadType.END,
+ count = 4,
+ placeholdersRemaining = 4
+ ),
+ outDrop
+ )
+ }
+
+ @Test
+ fun separatorRefresh() {
+ val initialState = mutableListOf<TransformablePage<String>>()
+ val outInsert = PageEvent.Insert.Refresh(
+ pages = listOf(
+ listOf("a2", "b1"),
+ listOf("c1", "c2")
+ ).toTransformablePages(),
+ placeholdersStart = 0,
+ placeholdersEnd = 1
+ ).insertSeparators(
+ currentPages = initialState
+ ) { before, after ->
+ if (before != null && after != null && before.first() != after.first())
+ after.first().toUpperCase().toString()
+ else null
+ }
+
+ assertEvent(
+ PageEvent.Insert.Refresh(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = 0,
+ data = listOf("a2", "B", "b1"),
+ originalPageSize = 2,
+ originalIndices = listOf(0, 1, 1)
+ ),
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("C"),
+ originalPageSize = 2,
+ originalIndices = listOf(0)
+ ),
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("c1", "c2"),
+ originalPageSize = 2,
+ originalIndices = listOf(0, 1)
+ )
+ ),
+ placeholdersStart = 0,
+ placeholdersEnd = 1
+ ),
+ outInsert
+ )
+ }
+
+ @Test
+ fun separatorEnd() {
+ val initialState =
+ listOf("a1", "a2")
+ .map { listOf(it) }
+ .toTransformablePages()
+ val outInsert = PageEvent.Insert.End(
+ pages = listOf(
+ listOf("c1", "d1"),
+ listOf("d2", "d3")
+ ).toTransformablePages(-1),
+ placeholdersEnd = 1
+ ).insertSeparators(
+ currentPages = initialState
+ ) { before, after ->
+ if (before != null && after != null && before.first() != after.first())
+ after.first().toUpperCase().toString()
+ else null
+ }
+
+ assertEvent(
+ PageEvent.Insert.End(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("C"),
+ originalPageSize = 2,
+ originalIndices = listOf(0)
+ ),
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("c1", "D", "d1"),
+ originalPageSize = 2,
+ originalIndices = listOf(0, 1, 1)
+ ),
+ TransformablePage(
+ originalPageOffset = 2,
+ data = listOf(),
+ originalPageSize = 2,
+ originalIndices = null
+ ),
+ TransformablePage(
+ originalPageOffset = 2,
+ data = listOf("d2", "d3"),
+ originalPageSize = 2,
+ originalIndices = listOf(0, 1)
+ )
+ ),
+ placeholdersEnd = 1
+ ),
+ outInsert
+ )
+ }
+
+ @Test
+ fun separatorStart() {
+ val initialState =
+ listOf("d1", "d2")
+ .map { listOf(it) }
+ .toTransformablePages()
+ val outInsert = PageEvent.Insert.Start(
+ pages = listOf(
+ listOf("a1", "b1"),
+ listOf("b2", "b3")
+ ).toTransformablePages(2),
+ placeholdersStart = 1
+ ).insertSeparators(
+ currentPages = initialState
+ ) { before, after ->
+ if (before != null && after != null && before.first() != after.first())
+ after.first().toUpperCase().toString()
+ else null
+ }
+
+ assertEvent(
+ PageEvent.Insert.Start(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = -2,
+ data = listOf("a1", "B", "b1"),
+ originalPageSize = 2,
+ originalIndices = listOf(0, 1, 1)
+ ),
+ TransformablePage(
+ originalPageOffset = -1,
+ data = listOf(),
+ originalPageSize = 2,
+ originalIndices = null
+ ),
+ TransformablePage(
+ originalPageOffset = -1,
+ data = listOf("b2", "b3"),
+ originalPageSize = 2,
+ originalIndices = listOf(0, 1)
+ ),
+ TransformablePage(
+ originalPageOffset = -1,
+ data = listOf("D"),
+ originalPageSize = 2,
+ originalIndices = listOf(1) // note: using last index of 2nd page in
+ )
+ ),
+ placeholdersStart = 1
+ ),
+ outInsert
+ )
+ }
+}
\ No newline at end of file
diff --git a/paging/common/src/test/kotlin/androidx/paging/TransformablePageTest.kt b/paging/common/src/test/kotlin/androidx/paging/TransformablePageTest.kt
index fba96e7..4e79930 100644
--- a/paging/common/src/test/kotlin/androidx/paging/TransformablePageTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/TransformablePageTest.kt
@@ -21,6 +21,21 @@
import org.junit.runners.JUnit4
import kotlin.test.assertEquals
+@Suppress("TestFunctionName")
+internal fun <T : Any> TransformablePage(data: List<T>) = TransformablePage(
+ data = data,
+ originalPageOffset = 0
+)
+
+internal fun <T : Any> List<List<T>>.toTransformablePages(
+ indexOfInitialPage: Int = 0
+) = mapIndexed { index, list ->
+ TransformablePage(
+ data = list,
+ originalPageOffset = index - indexOfInitialPage
+ )
+}.toMutableList()
+
@Suppress("SameParameterValue")
@RunWith(JUnit4::class)
class TransformablePageTest {
@@ -47,7 +62,7 @@
val page = TransformablePage(
data = listOf('a', 'b'),
originalPageOffset = -4,
- sourcePageSize = 30,
+ originalPageSize = 30,
originalIndices = listOf(10, 20)
)
// negative - index pass-through