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()))