PagingData.empty helpers now emit cached empty list
This allows paging-compose to display an empty list immediately upon initial composition. Also enables paging-compose to detect cached data so it can properly dispatch NotLoading states.
Test: ANDROIDX_PROJECTS=INFRAROGUE ./gradlew paging:paging-common:allTest
Test: ./gradlew paging:paging-compose:cC
Test: ./gradlew paging:paging-runtime:cC
Bug: 301833847
Relnote: "PagingData.empty() now dispatches NotLoading states by default unless custom LoadStates are passed to its constructor. This departs from existing behavior where it doesn't dispatch LoadStates when submitted to a PagingDataAdapter or it dispatches Loading states when collected as LazyPagingItems. When collected as LazyPagingItems, it will now also display an empty list immediately upon initial composition."
Change-Id: I4d11df131d81fb0234e096581b74440a4b076a76
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingData.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingData.kt
index f4e3a15..edb541f 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingData.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingData.kt
@@ -52,9 +52,9 @@
}
/**
- * Create a [PagingData] that immediately displays an empty list of items without
- * dispatching any load state updates when submitted to a presenter. E.g.,
- * [AsyncPagingDataAdapter][androidx.paging.AsyncPagingDataAdapter].
+ * Create a [PagingData] that immediately displays an empty list of items when submitted to
+ * a presenter. E.g., [AsyncPagingDataAdapter][androidx.paging.AsyncPagingDataAdapter] and
+ * dispatches [LoadState.NotLoading] on all LoadStates to the presenter.
*/
@Suppress("UNCHECKED_CAST")
@JvmStatic // Convenience for Java developers.
@@ -68,6 +68,20 @@
),
uiReceiver = NOOP_UI_RECEIVER,
hintReceiver = NOOP_HINT_RECEIVER,
+ cachedPageEvent = {
+ PageEvent.Insert.Refresh(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = 0,
+ data = listOf(),
+ )
+ ),
+ placeholdersBefore = 0,
+ placeholdersAfter = 0,
+ sourceLoadStates = LoadStates.IDLE,
+ mediatorLoadStates = null
+ )
+ }
)
/**
@@ -95,12 +109,26 @@
),
uiReceiver = NOOP_UI_RECEIVER,
hintReceiver = NOOP_HINT_RECEIVER,
+ cachedPageEvent = {
+ PageEvent.Insert.Refresh(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = 0,
+ data = listOf(),
+ )
+ ),
+ placeholdersBefore = 0,
+ placeholdersAfter = 0,
+ sourceLoadStates = sourceLoadStates,
+ mediatorLoadStates = mediatorLoadStates
+ )
+ }
)
/**
- * Create a [PagingData] that immediately displays a static list of items without
- * dispatching any load state updates when submitted to a presenter. E.g.,
- * [AsyncPagingDataAdapter][androidx.paging.AsyncPagingDataAdapter].
+ * Create a [PagingData] that immediately displays a static list of items when submitted
+ * to a presenter. E.g., [AsyncPagingDataAdapter][androidx.paging.AsyncPagingDataAdapter]
+ * and dispatches [LoadState.NotLoading] on all LoadStates to the presenter.
*
* @param data Static list of [T] to display.
*/
diff --git a/paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt b/paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt
index 4399ddd..34e074f 100644
--- a/paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt
+++ b/paging/paging-compose/src/androidInstrumentedTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt
@@ -41,17 +41,22 @@
import androidx.compose.ui.unit.dp
import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
+import androidx.paging.LoadState.Loading
+import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadStates
import androidx.paging.Pager
import androidx.paging.PagingConfig
+import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.TestPagingSource
import androidx.paging.cachedIn
+import androidx.paging.loadStates
import androidx.paging.localLoadStatesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -928,6 +933,167 @@
}
@Test
+ fun cachedPagingDataFrom() {
+ val flow = MutableStateFlow(PagingData.from(items))
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ val dispatcher = StandardTestDispatcher()
+ rule.setContent {
+ lazyPagingItems = flow.collectAsLazyPagingItems(dispatcher)
+ }
+
+ rule.waitForIdle()
+
+ // assert cached data is available right away prior to collection
+ assertThat(lazyPagingItems.itemSnapshotList).containsExactlyElementsIn(items).inOrder()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf()) // NotLoading
+ dispatcher.scheduler.advanceUntilIdle()
+ // assert data is still the same after load
+ assertThat(lazyPagingItems.itemSnapshotList).containsExactlyElementsIn(items).inOrder()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf()) // NotLoading
+ }
+
+ @Test
+ fun cachedPagingDataFromWithLoadStates() {
+ val flow = MutableStateFlow(
+ PagingData.from(
+ data = items,
+ sourceLoadStates = loadStates(refresh = Loading),
+ )
+ )
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ val dispatcher = StandardTestDispatcher()
+ rule.setContent {
+ lazyPagingItems = flow.collectAsLazyPagingItems(dispatcher)
+ }
+
+ rule.waitForIdle()
+
+ // assert cached data is available right away prior to collection
+ assertThat(lazyPagingItems.itemSnapshotList).containsExactlyElementsIn(items).inOrder()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf(refreshLocal = Loading))
+ dispatcher.scheduler.advanceUntilIdle()
+ // assert data is still the same after load
+ assertThat(lazyPagingItems.itemSnapshotList).containsExactlyElementsIn(items).inOrder()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf(refreshLocal = Loading))
+ }
+
+ @Test
+ fun cachedPagingDataFromWithEmptyData() {
+ val flow = MutableStateFlow(PagingData.from(emptyList<Int>()))
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ val dispatcher = StandardTestDispatcher()
+ rule.setContent {
+ lazyPagingItems = flow.collectAsLazyPagingItems(dispatcher)
+ }
+
+ rule.waitForIdle()
+
+ // assert before load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf()) // NotLoading
+ dispatcher.scheduler.advanceUntilIdle()
+ // assert data is still the same after load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf()) // NotLoading
+ }
+
+ @Test
+ fun cachedPagingDataFromWithEmptyDataAndLoadStates() {
+ val flow = MutableStateFlow(
+ PagingData.from(
+ emptyList<Int>(),
+ sourceLoadStates = loadStates(
+ prepend = NotLoading(true),
+ append = NotLoading(true)
+ )
+ )
+ )
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ val restorationTester = StateRestorationTester(rule)
+ val dispatcher = StandardTestDispatcher()
+ restorationTester.setContent {
+ lazyPagingItems = flow.collectAsLazyPagingItems(dispatcher)
+ }
+
+ rule.waitForIdle()
+
+ // assert before load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(
+ localLoadStatesOf(
+ prependLocal = NotLoading(true),
+ appendLocal = NotLoading(true)
+ )
+ )
+ dispatcher.scheduler.advanceUntilIdle()
+ // assert data is still the same after load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(
+ localLoadStatesOf(
+ prependLocal = NotLoading(true),
+ appendLocal = NotLoading(true)
+ )
+ )
+ }
+
+ @Test
+ fun cachedPagingDataEmpty() {
+ val flow = MutableStateFlow(PagingData.empty<Int>())
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ val dispatcher = StandardTestDispatcher()
+ rule.setContent {
+ lazyPagingItems = flow.collectAsLazyPagingItems(dispatcher)
+ }
+
+ rule.waitForIdle()
+
+ // assert before load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf()) // NotLoading
+ dispatcher.scheduler.advanceUntilIdle()
+ // assert data is still the same after load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(localLoadStatesOf()) // NotLoading
+ }
+
+ @Test
+ fun cachedPagingDataEmptyWithLoadStates() {
+ val flow = MutableStateFlow(
+ PagingData.empty<Int>(
+ sourceLoadStates = loadStates(
+ prepend = NotLoading(true),
+ append = NotLoading(true)
+ )
+ )
+ )
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ val dispatcher = StandardTestDispatcher()
+ rule.setContent {
+ lazyPagingItems = flow.collectAsLazyPagingItems(dispatcher)
+ }
+
+ rule.waitForIdle()
+
+ // assert before load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(
+ localLoadStatesOf(
+ prependLocal = NotLoading(true),
+ appendLocal = NotLoading(true)
+ )
+ )
+ dispatcher.scheduler.advanceUntilIdle()
+ // assert data is still the same after load
+ assertThat(lazyPagingItems.itemSnapshotList).isEmpty()
+ assertThat(lazyPagingItems.loadState).isEqualTo(
+ localLoadStatesOf(
+ prependLocal = NotLoading(true),
+ appendLocal = NotLoading(true)
+ )
+ )
+ }
+
+ @Test
fun cachedData_withPlaceholders() {
val flow = createPagerWithPlaceholders().flow
.cachedIn(TestScope(UnconfinedTestDispatcher()))