Implement LazyPagingItem itemContenType as helper function

Added itemContentType API which returns a lambda that can be plugged into `contentType` params for Lazy scopes.
If no contentType is provided, defaults to null contentType.

Test: Compose demos
Test: ./gradlew paging:paging-compose:cC
Bug: 259385813
Relnote: "LazyPagingItems has a new itemContentType API which returns a contentType implementation that can be plugged into the `contentType` params of Lazy scopes. If no contentType is provided, defaults to `null` contentType for all paging items."
Change-Id: Ib04f099111605af368c5b025d84a085ab4251fb6
diff --git a/paging/paging-compose/api/current.txt b/paging/paging-compose/api/current.txt
index 3ae7d53..da8a532 100644
--- a/paging/paging-compose/api/current.txt
+++ b/paging/paging-compose/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.paging.compose {
 
   public final class LazyFoundationExtensionsKt {
+    method public static <T> kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> itemContentType(androidx.paging.compose.LazyPagingItems<T>, optional kotlin.jvm.functions.Function1<T,?>? contentType);
     method public static <T> kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> itemKey(androidx.paging.compose.LazyPagingItems<T>, optional kotlin.jvm.functions.Function1<T,?>? key);
   }
 
diff --git a/paging/paging-compose/api/public_plus_experimental_current.txt b/paging/paging-compose/api/public_plus_experimental_current.txt
index 3ae7d53..da8a532 100644
--- a/paging/paging-compose/api/public_plus_experimental_current.txt
+++ b/paging/paging-compose/api/public_plus_experimental_current.txt
@@ -2,6 +2,7 @@
 package androidx.paging.compose {
 
   public final class LazyFoundationExtensionsKt {
+    method public static <T> kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> itemContentType(androidx.paging.compose.LazyPagingItems<T>, optional kotlin.jvm.functions.Function1<T,?>? contentType);
     method public static <T> kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> itemKey(androidx.paging.compose.LazyPagingItems<T>, optional kotlin.jvm.functions.Function1<T,?>? key);
   }
 
diff --git a/paging/paging-compose/api/restricted_current.txt b/paging/paging-compose/api/restricted_current.txt
index 3ae7d53..da8a532 100644
--- a/paging/paging-compose/api/restricted_current.txt
+++ b/paging/paging-compose/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.paging.compose {
 
   public final class LazyFoundationExtensionsKt {
+    method public static <T> kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> itemContentType(androidx.paging.compose.LazyPagingItems<T>, optional kotlin.jvm.functions.Function1<T,?>? contentType);
     method public static <T> kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> itemKey(androidx.paging.compose.LazyPagingItems<T>, optional kotlin.jvm.functions.Function1<T,?>? key);
   }
 
diff --git a/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/PagingFoundationDemos.kt b/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/PagingFoundationDemos.kt
index a5d27d0..c63c4b7 100644
--- a/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/PagingFoundationDemos.kt
+++ b/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/PagingFoundationDemos.kt
@@ -20,6 +20,7 @@
 import androidx.compose.integration.demos.common.DemoCategory
 import androidx.paging.compose.samples.PagingWithHorizontalPager
 import androidx.paging.compose.samples.PagingWithLazyGrid
+import androidx.paging.compose.samples.PagingWithLazyList
 import androidx.paging.compose.samples.PagingWithVerticalPager
 
 val PagingFoundationDemos = DemoCategory(
@@ -27,6 +28,7 @@
     listOf(
         ComposableDemo("Paging with HorizontalPager") { PagingWithHorizontalPager() },
         ComposableDemo("Paging with VerticalPager") { PagingWithVerticalPager() },
-        ComposableDemo("Paging with LazyGrid") { PagingWithLazyGrid() }
+        ComposableDemo("Paging with LazyGrid") { PagingWithLazyGrid() },
+        ComposableDemo("Paging with LazyColumn") { PagingWithLazyList() },
     )
 )
\ No newline at end of file
diff --git a/paging/paging-compose/samples/src/main/java/androidx/paging/compose/samples/PagingFoundationSample.kt b/paging/paging-compose/samples/src/main/java/androidx/paging/compose/samples/PagingFoundationSample.kt
index 5867a4c..8b55ca8 100644
--- a/paging/paging-compose/samples/src/main/java/androidx/paging/compose/samples/PagingFoundationSample.kt
+++ b/paging/paging-compose/samples/src/main/java/androidx/paging/compose/samples/PagingFoundationSample.kt
@@ -24,6 +24,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.pager.HorizontalPager
@@ -41,6 +42,7 @@
 import androidx.paging.PagingConfig
 import androidx.paging.TestPagingSource
 import androidx.paging.compose.collectAsLazyPagingItems
+import androidx.paging.compose.itemContentType
 import androidx.paging.compose.itemKey
 
 val pager = Pager(
@@ -94,6 +96,39 @@
         items(
             count = lazyPagingItems.itemCount,
             key = lazyPagingItems.itemKey { it },
+            contentType = lazyPagingItems.itemContentType { "MyPagingItems" }
+        ) { index ->
+            val item = lazyPagingItems[index]
+            PagingItem(item = item)
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Sampled
+@Composable
+public fun PagingWithLazyList() {
+    val lazyPagingItems = pager.collectAsLazyPagingItems()
+
+    LazyColumn {
+        stickyHeader(
+            key = "Header",
+            contentType = "My Header",
+        ) {
+            Box(
+                modifier = Modifier
+                    .padding(bottom = 10.dp)
+                    .background(Color.Red)
+                    .fillMaxWidth(),
+                contentAlignment = Alignment.Center
+            ) {
+                Text(text = "Header", fontSize = 32.sp)
+            }
+        }
+        items(
+            count = lazyPagingItems.itemCount,
+            key = lazyPagingItems.itemKey { it },
+            contentType = lazyPagingItems.itemContentType { "MyPagingItems" }
         ) { index ->
             val item = lazyPagingItems[index]
             PagingItem(item = item)
diff --git a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
index c0a818d..1e59f5a 100644
--- a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
+++ b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
@@ -61,6 +61,8 @@
     val rule = createComposeRule()
 
     val items = (1..10).toList().map { it }
+    private val itemsSizePx = 30f
+    private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
 
     private fun createPager(
         config: PagingConfig = PagingConfig(
@@ -77,6 +79,19 @@
         return Pager(config = config, pagingSourceFactory = pagingSourceFactory)
     }
 
+    private fun createPagerWithPlaceholders(
+        config: PagingConfig = PagingConfig(
+            pageSize = 1,
+            enablePlaceholders = true,
+            maxSize = 200,
+            initialLoadSize = 3,
+            prefetchDistance = 0,
+        )
+    ) = Pager(
+        config = config,
+        pagingSourceFactory = { TestPagingSource(items = items, loadDelay = 0) }
+    )
+
     @Test
     fun lazyPagingInitialLoadState() {
         val pager = createPager()
@@ -251,11 +266,8 @@
 
     @Test
     fun differentContentTypes() {
-        val pager = createPager()
-
+        val pager = createPagerWithPlaceholders()
         lateinit var state: LazyListState
-        val itemsSizePx = 30f
-        val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
 
         rule.setContent {
             state = rememberLazyListState()
@@ -267,20 +279,17 @@
             }
 
             LazyColumn(Modifier.height(itemsSizeDp * 2.5f), state) {
-                val content = @Composable { tag: String ->
-                    Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag))
-                }
                 item(contentType = "not-to-reuse--1") {
-                    content("-1")
+                    Content("-1")
                 }
                 item(contentType = "reuse") {
-                    content("0")
+                    Content("0")
                 }
                 items(
                     items = lazyPagingItems,
                     contentType = { if (it == 8) "reuse" else "not-to-reuse-$it" }
                 ) {
-                    content("$it")
+                    Content("$it")
                 }
             }
         }
@@ -302,7 +311,7 @@
         rule.runOnIdle {
             runBlocking {
                 state.scrollToItem(8)
-                // item 8 should reuse slot 1
+                // item 8 should reuse slot 0
             }
         }
 
@@ -321,22 +330,9 @@
     }
 
     @Test
-    fun nullContentTypeWithPlaceholders() {
-        val config = PagingConfig(
-            pageSize = 1,
-            enablePlaceholders = true,
-            maxSize = 200,
-            initialLoadSize = 3,
-            prefetchDistance = 0,
-        )
-        val pager = Pager(
-            config = config,
-            pagingSourceFactory = { TestPagingSource(items = items, loadDelay = 0) }
-        )
-
+    fun nullItemContentType() {
+        val pager = createPagerWithPlaceholders()
         lateinit var state: LazyListState
-        val itemsSizePx = 30f
-        val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
 
         var loadedItem6 = false
 
@@ -351,22 +347,19 @@
             }
 
             LazyColumn(Modifier.height(itemsSizeDp * 2.5f), state) {
-                val content = @Composable { tag: String ->
-                    Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag))
-                }
                 item(contentType = "not-to-reuse--1") {
-                    content("-1")
+                    Content("-1")
                 }
-                item(contentType = null) {
-                    content("0")
+                // to be reused later by placeholder item
+                item(contentType = PagingPlaceholderContentType) {
+                    Content("0")
                 }
                 items(
                     items = lazyPagingItems,
-                    key = { it },
-                    // item 7 would be null, which should default to contentType null
+                    // item 7 would be null, which should default to PagingPlaceholderContentType
                     contentType = { "not-to-reuse-$it" }
                 ) {
-                    content("$it")
+                    Content("$it")
                 }
             }
         }
@@ -392,7 +385,7 @@
         rule.runOnIdle {
             runBlocking {
                 state.scrollToItem(6)
-                // item 7 which is null should reuse slot 1
+                // item 7 which is null should reuse slot 0
             }
         }
 
@@ -402,9 +395,69 @@
         // node reused
         rule.onNodeWithTag("0")
             .assertDoesNotExist()
-        rule.onNodeWithTag("null")
+    }
+
+    @Test
+    fun nullContentType() {
+        val pager = createPagerWithPlaceholders()
+        lateinit var state: LazyListState
+
+        rule.setContent {
+            state = rememberLazyListState()
+
+            val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
+            for (i in 0 until lazyPagingItems.itemCount) {
+                lazyPagingItems[i]
+            }
+
+            LazyColumn(Modifier.height(itemsSizeDp * 2.5f), state) {
+                item(contentType = "not-to-reuse--1") {
+                    Content("-1")
+                }
+                // to be reused later by real items
+                item(contentType = null) {
+                    Content("0")
+                }
+                items(
+                    items = lazyPagingItems,
+                    // should default to null
+                    contentType = null
+                ) {
+                    Content("$it")
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(2)
+                // now items -1 and 0 are put into reusables
+            }
+        }
+
+        rule.onNodeWithTag("-1")
             .assertExists()
             .assertIsNotDisplayed()
+        rule.onNodeWithTag("0")
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            runBlocking {
+                // item 4
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.onNodeWithTag("-1")
+            .assertExists()
+            .assertIsNotDisplayed()
+        // node reused
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("4")
+            .assertExists()
+            .assertIsDisplayed()
     }
 
     @Test
@@ -848,4 +901,9 @@
             items
         )
     }
+
+    @Composable
+    private fun Content(tag: String) {
+        Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag))
+    }
 }
diff --git a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyFoundationExtensions.kt b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyFoundationExtensions.kt
index 1f00063..e4c2610 100644
--- a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyFoundationExtensions.kt
+++ b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyFoundationExtensions.kt
@@ -50,3 +50,33 @@
         }
     }
 }
+
+/**
+ * Returns a factory for the content type of the item.
+ *
+ * ContentTypes are generated with the contentType lambda that is passed in. If null is passed in,
+ * contentType of all items will default to `null`.
+ * If [PagingConfig.enablePlaceholders] is true, LazyPagingItems may return null items. Null
+ * items will automatically default to placeholder contentType.
+ *
+ * This factory can be applied to Lazy foundations such as [LazyGridScope.items] or Pagers.
+ * Examples:
+ * @sample androidx.paging.compose.samples.PagingWithLazyGrid
+ * @sample androidx.paging.compose.samples.PagingWithLazyList
+ *
+ * @param [contentType] a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of
+ * such type will be considered compatible.
+ */
+public fun <T : Any> LazyPagingItems<T>.itemContentType(
+    contentType: ((item: @JvmSuppressWildcards T) -> Any?)? = null
+): (index: Int) -> Any? {
+    return { index ->
+        if (contentType == null) {
+            null
+        } else {
+            val item = peek(index)
+            if (item == null) PagingPlaceholderContentType else contentType(item)
+        }
+    }
+}
diff --git a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
index fa2cbda..2614263 100644
--- a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
+++ b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
@@ -308,12 +308,7 @@
     items(
         count = items.itemCount,
         key = items.itemKey(key),
-        contentType = { index ->
-            if (contentType == null) null else {
-                val item = items.peek(index)
-                if (item == null) null else contentType(item)
-            }
-        }
+        contentType = items.itemContentType(contentType)
     ) { index ->
         itemContent(items[index])
     }
diff --git a/paging/paging-compose/src/main/java/androidx/paging/compose/PagingPlaceholderKey.kt b/paging/paging-compose/src/main/java/androidx/paging/compose/PagingPlaceholders.kt
similarity index 96%
rename from paging/paging-compose/src/main/java/androidx/paging/compose/PagingPlaceholderKey.kt
rename to paging/paging-compose/src/main/java/androidx/paging/compose/PagingPlaceholders.kt
index ac98161..417f2e3 100644
--- a/paging/paging-compose/src/main/java/androidx/paging/compose/PagingPlaceholderKey.kt
+++ b/paging/paging-compose/src/main/java/androidx/paging/compose/PagingPlaceholders.kt
@@ -41,4 +41,6 @@
                 override fun newArray(size: Int) = arrayOfNulls<PagingPlaceholderKey?>(size)
             }
     }
-}
\ No newline at end of file
+}
+
+internal object PagingPlaceholderContentType