Merge "Update proto docs for class name" into androidx-main
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index 4339957..c8e0c04 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -111,12 +111,6 @@
     method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
-  }
-
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
-  }
-
   public final class IntrinsicKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 3e7c323..3d72dbe 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -114,7 +114,7 @@
   @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
+  @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
   }
 
   public final class FlowLayoutKt {
@@ -122,7 +122,7 @@
     method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Composable public static inline void FlowRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional int maxItemsInEachRow, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.FlowRowScope,kotlin.Unit> content);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
+  @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
   }
 
   public final class IntrinsicKt {
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 2b75f19..03cdfb3 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -114,17 +114,11 @@
     method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
-  }
-
   public final class FlowLayoutKt {
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy columnMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, int maxItemsInMainAxis);
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy rowMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, int maxItemsInMainAxis);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
-  }
-
   public final class IntrinsicKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
index 5d58064..cdec1d5 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
@@ -126,7 +126,7 @@
  */
 @LayoutScopeMarker
 @Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
 interface FlowRowScope : RowScope
 
 /**
@@ -134,11 +134,13 @@
  */
 @LayoutScopeMarker
 @Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
 interface FlowColumnScope : ColumnScope
 
+@OptIn(ExperimentalLayoutApi::class)
 internal object FlowRowScopeInstance : RowScope by RowScopeInstance, FlowRowScope
 
+@OptIn(ExperimentalLayoutApi::class)
 internal object FlowColumnScopeInstance : ColumnScope by ColumnScopeInstance, FlowColumnScope
 
 private fun getVerticalArrangement(verticalArrangement: Arrangement.Vertical):
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
index 78210e2..b0a80c7 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
@@ -38,7 +38,6 @@
 import androidx.compose.ui.unit.constrain
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
 
 /**
@@ -768,12 +767,12 @@
     private val Density.targetConstraints: Constraints
         get() {
             val maxWidth = if (maxWidth != Dp.Unspecified) {
-                maxWidth.coerceAtLeast(0.dp).roundToPx()
+                maxWidth.roundToPx().coerceAtLeast(0)
             } else {
                 Constraints.Infinity
             }
             val maxHeight = if (maxHeight != Dp.Unspecified) {
-                maxHeight.coerceAtLeast(0.dp).roundToPx()
+                maxHeight.roundToPx().coerceAtLeast(0)
             } else {
                 Constraints.Infinity
             }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index d0fc90e..603ac21 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -94,7 +94,10 @@
     @Test
     fun measureAndPlaceTwoItems() {
         val itemProvider = itemProvider({ 2 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index"))
         }
         rule.setContent {
             LazyLayout(itemProvider) {
@@ -118,8 +121,14 @@
     @Test
     fun measureAndPlaceMultipleLayoutsInOneItem() {
         val itemProvider = itemProvider({ 1 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("${index}x0"))
-            Box(Modifier.fillMaxSize().testTag("${index}x1"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("${index}x0"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("${index}x1"))
         }
 
         rule.setContent {
@@ -143,7 +152,10 @@
     @Test
     fun updatingitemProvider() {
         var itemProvider by mutableStateOf(itemProvider({ 1 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index"))
         })
 
         rule.setContent {
@@ -166,7 +178,10 @@
 
         rule.runOnIdle {
             itemProvider = itemProvider({ 2 }) { index ->
-                Box(Modifier.fillMaxSize().testTag("$index"))
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag("$index"))
             }
         }
 
@@ -178,7 +193,10 @@
     fun stateBaseditemProvider() {
         var itemCount by mutableStateOf(1)
         val itemProvider = itemProvider({ itemCount }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index"))
         }
 
         rule.setContent {
@@ -228,7 +246,11 @@
             }
         }
         val itemProvider = itemProvider({ 1 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index").then(modifier))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index")
+                    .then(modifier))
         }
         var needToCompose by mutableStateOf(false)
         val prefetchState = LazyLayoutPrefetchState()
@@ -335,13 +357,15 @@
     fun nodeIsReusedWithoutExtraRemeasure() {
         var indexToCompose by mutableStateOf<Int?>(0)
         var remeasuresCount = 0
-        val modifier = Modifier.layout { measurable, constraints ->
-            val placeable = measurable.measure(constraints)
-            remeasuresCount++
-            layout(placeable.width, placeable.height) {
-                placeable.place(0, 0)
+        val modifier = Modifier
+            .layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                remeasuresCount++
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
             }
-        }.fillMaxSize()
+            .fillMaxSize()
         val itemProvider = itemProvider({ 2 }) {
             Box(modifier)
         }
@@ -376,6 +400,52 @@
     }
 
     @Test
+    fun nodeIsReusedWhenRemovedFirst() {
+        var itemCount by mutableStateOf(1)
+        var remeasuresCount = 0
+        val modifier = Modifier
+            .layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                remeasuresCount++
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
+            }
+            .fillMaxSize()
+        val itemProvider = itemProvider({ itemCount }) {
+            Box(modifier)
+        }
+
+        rule.setContent {
+            LazyLayout(itemProvider) { constraints ->
+                val node = if (itemCount == 1) {
+                    measure(0, constraints).first()
+                } else {
+                    null
+                }
+                layout(10, 10) {
+                    node?.place(0, 0)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasuresCount).isEqualTo(1)
+            // node will be kept for reuse
+            itemCount = 0
+        }
+
+        rule.runOnIdle {
+            // node should be now reused
+            itemCount = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasuresCount).isEqualTo(1)
+        }
+    }
+
+    @Test
     fun regularCompositionIsUsedInPrefetchTimeCalculation() {
         val itemProvider = itemProvider({ 1 }) {
             Box(Modifier.fillMaxSize())
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 910e79f..748d6b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -89,7 +89,7 @@
 )
 
 private class BackgroundElement(
-    private val color: Color? = null,
+    private val color: Color = Color.Unspecified,
     private val brush: Brush? = null,
     private val alpha: Float,
     private val shape: Shape,
@@ -116,7 +116,7 @@
     }
 
     override fun hashCode(): Int {
-        var result = color?.hashCode() ?: 0
+        var result = color.hashCode()
         result = 31 * result + (brush?.hashCode() ?: 0)
         result = 31 * result + alpha.hashCode()
         result = 31 * result + shape.hashCode()
@@ -133,7 +133,7 @@
 }
 
 private class BackgroundNode(
-    var color: Color?,
+    var color: Color,
     var brush: Brush?,
     var alpha: Float,
     var shape: Shape,
@@ -155,7 +155,7 @@
     }
 
     private fun ContentDrawScope.drawRect() {
-        color?.let { drawRect(color = it) }
+        if (color != Color.Unspecified) drawRect(color = color)
         brush?.let { drawRect(brush = it, alpha = alpha) }
     }
 
@@ -166,7 +166,7 @@
             } else {
                 shape.createOutline(size, layoutDirection, this)
             }
-        color?.let { drawOutline(outline, color = it) }
+        if (color != Color.Unspecified) drawOutline(outline, color = color)
         brush?.let { drawOutline(outline, brush = it, alpha = alpha) }
         lastOutline = outline
         lastSize = size
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
index 4023a81..9b61189 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.ReusableContentHost
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -95,13 +96,11 @@
             val index = itemProvider.findIndexByKey(key, lastKnownIndex).also {
                 lastKnownIndex = it
             }
-
-            if (index < itemProvider.itemCount) {
-                val key = itemProvider.getKey(index)
-                if (key == this.key) {
-                    StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
-                        itemProvider.Item(index)
-                    }
+            val indexIsUpToDate =
+                index < itemProvider.itemCount && itemProvider.getKey(index) == key
+            ReusableContentHost(active = indexIsUpToDate) {
+                StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
+                    itemProvider.Item(index)
                 }
             }
             DisposableEffect(key) {
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -7,7 +7,8 @@
   }
 
   public final class AndroidMenu_androidKt {
-    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index d9fe905..feb60c4b 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -7,7 +7,8 @@
   }
 
   public final class AndroidMenu_androidKt {
-    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
@@ -426,7 +427,7 @@
   }
 
   @androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface ExposedDropdownMenuBoxScope {
-    method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.ScrollState scrollState, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method public androidx.compose.ui.Modifier exposedDropdownSize(androidx.compose.ui.Modifier, optional boolean matchTextFieldWidth);
   }
 
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -7,7 +7,8 @@
   }
 
   public final class AndroidMenu_androidKt {
-    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
index ccbd6ff..30fa679 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
@@ -50,6 +50,7 @@
 import androidx.compose.material.samples.LeadingIconTabs
 import androidx.compose.material.samples.LinearProgressIndicatorSample
 import androidx.compose.material.samples.MenuSample
+import androidx.compose.material.samples.MenuWithScrollStateSample
 import androidx.compose.material.samples.ModalBottomSheetSample
 import androidx.compose.material.samples.ModalDrawerSample
 import androidx.compose.material.samples.NavigationRailBottomAlignSample
@@ -398,6 +399,13 @@
         MenuSample()
     },
     Example(
+        name = ::MenuWithScrollStateSample.name,
+        description = MenusExampleDescription,
+        sourceUrl = MenusExampleSourceUrl
+    ) {
+        MenuWithScrollStateSample()
+    },
+    Example(
         name = ::ExposedDropdownMenuSample.name,
         description = MenusExampleDescription,
         sourceUrl = MenusExampleSourceUrl
@@ -703,14 +711,18 @@
         description = TextFieldsExampleDescription,
         sourceUrl = TextFieldsExampleSourceUrl
     ) {
-       TextArea()
+        TextArea()
     }
 ).map {
     // By default text field samples are minimal and don't have a `width` modifier to restrict the
     // width. As a result, they grow horizontally if enough text is typed. To prevent this behavior
     // in Catalog app the code below restricts the width of every text field sample
     it.copy(content = {
-        Box(Modifier.wrapContentWidth().width(280.dp)) { it.content() }
+        Box(
+            Modifier
+                .wrapContentWidth()
+                .width(280.dp)
+        ) { it.content() }
     })
 }
 
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
index bf6f420..f9c6a77 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.material.Divider
 import androidx.compose.material.DropdownMenu
 import androidx.compose.material.DropdownMenuItem
@@ -29,6 +30,7 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -41,7 +43,9 @@
 fun MenuSample() {
     var expanded by remember { mutableStateOf(false) }
 
-    Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+    Box(modifier = Modifier
+        .fillMaxSize()
+        .wrapContentSize(Alignment.TopStart)) {
         IconButton(onClick = { expanded = true }) {
             Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
         }
@@ -61,4 +65,37 @@
             }
         }
     }
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun MenuWithScrollStateSample() {
+    var expanded by remember { mutableStateOf(false) }
+    val scrollState = rememberScrollState()
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .wrapContentSize(Alignment.TopStart)
+    ) {
+        IconButton(onClick = { expanded = true }) {
+            Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
+        }
+        DropdownMenu(
+            expanded = expanded,
+            onDismissRequest = { expanded = false },
+            scrollState = scrollState
+        ) {
+            repeat(30) {
+                DropdownMenuItem(onClick = { /* Handle item! */ }) {
+                    Text("Item ${it + 1}")
+                }
+            }
+        }
+        LaunchedEffect(expanded) {
+            if (expanded) {
+                // Scroll to show the bottom menu items.
+                scrollState.scrollTo(scrollState.maxValue)
+            }
+        }
+    }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
index 17c6cc8..23300d9 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
@@ -22,7 +22,9 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -35,9 +37,11 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.assertTextContains
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -336,6 +340,47 @@
         // Should not have crashed.
     }
 
+    @Test
+    fun withScrolledContent() {
+        rule.setMaterialContent {
+            Box(Modifier.fillMaxSize()) {
+                ExposedDropdownMenuBox(
+                    modifier = Modifier.align(Alignment.Center),
+                    expanded = true,
+                    onExpandedChange = { }
+                ) {
+                    val scrollState = rememberScrollState()
+                    TextField(
+                        value = "",
+                        onValueChange = { },
+                        label = { Text("Label") },
+                    )
+                    ExposedDropdownMenu(
+                        expanded = true,
+                        onDismissRequest = { },
+                        scrollState = scrollState
+                    ) {
+                        repeat(100) {
+                            Box(
+                                Modifier
+                                    .testTag("MenuContent ${it + 1}")
+                                    .size(with(LocalDensity.current) { 70.toDp() })
+                            )
+                        }
+                    }
+                    LaunchedEffect(Unit) {
+                        scrollState.scrollTo(scrollState.maxValue)
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+        rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+    }
+
     @Composable
     fun ExposedDropdownMenuForTest(
         expanded: Boolean,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index ace0f2a..8ad3242 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -21,6 +21,8 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -29,6 +31,8 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.hasAnyDescendant
 import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.isPopup
@@ -128,6 +132,42 @@
     }
 
     @Test
+    fun menu_scrolledContent() {
+        rule.setContent {
+            with(LocalDensity.current) {
+                Box(
+                    Modifier
+                        .requiredSize(20.toDp())
+                        .background(color = Color.Blue)
+                ) {
+                    val scrollState = rememberScrollState()
+                    DropdownMenu(
+                        expanded = true,
+                        onDismissRequest = {},
+                        scrollState = scrollState
+                    ) {
+                        repeat(100) {
+                            Box(
+                                Modifier
+                                    .testTag("MenuContent ${it + 1}")
+                                    .size(70.toDp())
+                            )
+                        }
+                    }
+                    LaunchedEffect(Unit) {
+                        scrollState.scrollTo(scrollState.maxValue)
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+        rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+    }
+
+    @Test
     fun menu_positioning_bottomEnd() {
         val screenWidth = 500
         val screenHeight = 1000
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
index 94c0373..e1bd14d 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
@@ -17,11 +17,13 @@
 package androidx.compose.material
 
 import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -70,6 +72,15 @@
  * tapping outside the menu's bounds
  * @param offset [DpOffset] to be added to the position of the menu
  */
+@Deprecated(
+    level = DeprecationLevel.HIDDEN,
+    replaceWith = ReplaceWith(
+        expression = "DropdownMenu(expanded,onDismissRequest, modifier, offset, " +
+            "rememberScrollState(), properties, content)",
+        "androidx.compose.foundation.rememberScrollState"
+    ),
+    message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
 @Composable
 fun DropdownMenu(
     expanded: Boolean,
@@ -78,6 +89,69 @@
     offset: DpOffset = DpOffset(0.dp, 0.dp),
     properties: PopupProperties = PopupProperties(focusable = true),
     content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+    expanded = expanded,
+    onDismissRequest = onDismissRequest,
+    modifier = modifier,
+    offset = offset,
+    scrollState = rememberScrollState(),
+    properties = properties,
+    content = content
+)
+
+/**
+ * <a href="https://material.io/components/menus#dropdown-menu" class="external" target="_blank">Material Design dropdown menu</a>.
+ *
+ * A dropdown menu is a compact way of displaying multiple choices. It appears upon interaction with
+ * an element (such as an icon or button) or when users perform a specific action.
+ *
+ * ![Menus image](https://developer.android.com/images/reference/androidx/compose/material/menus.png)
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded whether the menu is expanded or not
+ * @param onDismissRequest called when the user requests to dismiss the menu, such as by tapping
+ * outside the menu's bounds
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param properties [PopupProperties] for further customization of this popup's behavior
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    modifier: Modifier = Modifier,
+    offset: DpOffset = DpOffset(0.dp, 0.dp),
+    scrollState: ScrollState = rememberScrollState(),
+    properties: PopupProperties = PopupProperties(focusable = true),
+    content: @Composable ColumnScope.() -> Unit
 ) {
     val expandedStates = remember { MutableTransitionState(false) }
     expandedStates.targetState = expanded
@@ -100,6 +174,7 @@
             DropdownMenuContent(
                 expandedStates = expandedStates,
                 transformOriginState = transformOriginState,
+                scrollState = scrollState,
                 modifier = modifier,
                 content = content
             )
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
index 4a46e56..660f1c2 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
@@ -21,6 +21,7 @@
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.MutableTransitionState
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.gestures.waitForUpOrCancellation
@@ -30,6 +31,7 @@
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ArrowDropDown
 import androidx.compose.material.internal.ExposedDropdownMenuPopup
@@ -223,6 +225,7 @@
      * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
      * tapping outside the menu's bounds
      * @param modifier The modifier to apply to this layout
+     * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
      * @param content The content of the [ExposedDropdownMenu]
      */
     @Composable
@@ -230,6 +233,7 @@
         expanded: Boolean,
         onDismissRequest: () -> Unit,
         modifier: Modifier = Modifier,
+        scrollState: ScrollState = rememberScrollState(),
         content: @Composable ColumnScope.() -> Unit
     ) {
         // TODO(b/202810604): use DropdownMenu when PopupProperties constructor is stable
@@ -261,6 +265,7 @@
                 DropdownMenuContent(
                     expandedStates = expandedStates,
                     transformOriginState = transformOriginState,
+                    scrollState = scrollState,
                     modifier = modifier.exposedDropdownSize(),
                     content = content
                 )
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index 3e80837..9d3dadbd 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -21,8 +21,9 @@
 import androidx.compose.animation.core.animateFloat
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.IntrinsicSize
@@ -33,14 +34,13 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -57,11 +57,11 @@
 import kotlin.math.max
 import kotlin.math.min
 
-@Suppress("ModifierParameter")
 @Composable
 internal fun DropdownMenuContent(
     expandedStates: MutableTransitionState<Boolean>,
     transformOriginState: MutableState<TransformOrigin>,
+    scrollState: ScrollState,
     modifier: Modifier = Modifier,
     content: @Composable ColumnScope.() -> Unit
 ) {
@@ -126,7 +126,7 @@
             modifier = modifier
                 .padding(vertical = DropdownMenuVerticalPadding)
                 .width(IntrinsicSize.Max)
-                .verticalScroll(rememberScrollState()),
+                .verticalScroll(scrollState),
             content = content
         )
     }
diff --git a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
index 4da69f2..c197e65 100644
--- a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
+++ b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
@@ -21,6 +21,8 @@
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.mutableStateOf
@@ -74,7 +76,15 @@
  * @param offset [DpOffset] to be added to the position of the menu
  * @param content content lambda
  */
-@Suppress("ModifierParameter")
+@Deprecated(
+    level = DeprecationLevel.HIDDEN,
+    replaceWith = ReplaceWith(
+        expression = "DropdownMenu(expanded,onDismissRequest, focusable, modifier, offset, " +
+            "rememberScrollState(), content)",
+        "androidx.compose.foundation.rememberScrollState"
+    ),
+    message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
 @Composable
 fun DropdownMenu(
     expanded: Boolean,
@@ -83,6 +93,64 @@
     modifier: Modifier = Modifier,
     offset: DpOffset = DpOffset(0.dp, 0.dp),
     content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+    expanded = expanded,
+    onDismissRequest = onDismissRequest,
+    focusable = focusable,
+    modifier = modifier,
+    offset = offset,
+    scrollState = rememberScrollState(),
+    content = content
+)
+
+/**
+ * A Material Design [dropdown menu](https://material.io/components/menus#dropdown-menu).
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    focusable: Boolean = true,
+    modifier: Modifier = Modifier,
+    offset: DpOffset = DpOffset(0.dp, 0.dp),
+    scrollState: ScrollState = rememberScrollState(),
+    content: @Composable ColumnScope.() -> Unit
 ) {
     val expandedStates = remember { MutableTransitionState(false) }
     expandedStates.targetState = expanded
@@ -110,6 +178,7 @@
                 expandedStates = expandedStates,
                 transformOriginState = transformOriginState,
                 modifier = modifier,
+                scrollState = scrollState,
                 content = content
             )
         }
@@ -164,7 +233,15 @@
  * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
  * tapping outside the menu's bounds
  */
-@Suppress("ModifierParameter")
+@Deprecated(
+    level = DeprecationLevel.HIDDEN,
+    replaceWith = ReplaceWith(
+        expression = "CursorDropdownMenu(expanded,onDismissRequest, focusable, modifier, " +
+            "rememberScrollState(), content)",
+        "androidx.compose.foundation.rememberScrollState"
+    ),
+    message = "Replaced by a CursorDropdownMenu function with a ScrollState parameter"
+)
 @Composable
 fun CursorDropdownMenu(
     expanded: Boolean,
@@ -172,6 +249,40 @@
     focusable: Boolean = true,
     modifier: Modifier = Modifier,
     content: @Composable ColumnScope.() -> Unit
+) = CursorDropdownMenu(
+    expanded = expanded,
+    onDismissRequest = onDismissRequest,
+    focusable = focusable,
+    modifier = modifier,
+    scrollState = rememberScrollState(),
+    content = content
+)
+
+/**
+ *
+ * A [CursorDropdownMenu] behaves similarly to [Popup] and will use the current position of the mouse
+ * cursor to position itself on screen.
+ *
+ * The [content] of a [CursorDropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus.
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun CursorDropdownMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    focusable: Boolean = true,
+    modifier: Modifier = Modifier,
+    scrollState: ScrollState = rememberScrollState(),
+    content: @Composable ColumnScope.() -> Unit
 ) {
     val expandedStates = remember { MutableTransitionState(false) }
     expandedStates.targetState = expanded
@@ -188,6 +299,7 @@
                 expandedStates = expandedStates,
                 transformOriginState = transformOriginState,
                 modifier = modifier,
+                scrollState = scrollState,
                 content = content
             )
         }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
index 7374b08..1f62b814 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
@@ -31,6 +31,7 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Assume.assumeFalse
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -64,6 +65,8 @@
 
     @Test
     fun darkTheme() {
+        assumeFalse("See b/272301182", Build.VERSION.SDK_INT == 33)
+
         composeTestRule.setMaterialContent(darkColorScheme()) {
             Column(Modifier.testTag(Tag)) {
                 Spacer(Modifier.size(10.dp))
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
index 0df58c9..26f91f1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
@@ -146,7 +146,6 @@
         }
     }
 
-    @OptIn(ExperimentalMaterial3Api::class)
     @Test
     fun menu_scrolledContent() {
         rule.setContent {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
index 3693245..3c05d96 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.runtime.Composable
@@ -33,6 +34,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -112,6 +114,25 @@
     }
 
     @Test
+    fun lightTheme_customHeight() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationBar(interactionSource, Modifier.height(64.dp))
+        }
+
+        assertNavigationBarMatches(
+            scope = scope!!,
+            interactionSource = interactionSource,
+            interaction = null,
+            goldenIdentifier = "navigationBar_lightTheme_customHeight"
+        )
+    }
+
+    @Test
     fun darkTheme_defaultColors() {
         val interactionSource = MutableInteractionSource()
 
@@ -211,14 +232,16 @@
  *
  * @param interactionSource the [MutableInteractionSource] for the first [NavigationBarItem], to
  * control its visual state.
+ * @param modifier the [Modifier] applied to the navigation bar
  * @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
  */
 @Composable
 private fun DefaultNavigationBar(
     interactionSource: MutableInteractionSource,
+    modifier: Modifier = Modifier,
     setUnselectedItemsAsDisabled: Boolean = false,
 ) {
-    Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+    Box(modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
         NavigationBar {
             NavigationBarItem(
                 icon = { Icon(Icons.Filled.Favorite, null) },
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
index e9a0168..2f6c21f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material3.tokens.NavigationBarTokens
@@ -266,42 +267,43 @@
     @Test
     fun navigationBarItemContent_withLabel_sizeAndPosition() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box {
-                NavigationBar {
-                    NavigationBarItem(
-                        modifier = Modifier.testTag("item"),
-                        icon = {
-                            Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
-                        },
-                        label = {
-                            Text("ItemText")
-                        },
-                        selected = true,
-                        onClick = {}
-                    )
-                }
+            NavigationBar {
+                NavigationBarItem(
+                    modifier = Modifier.testTag("item"),
+                    icon = {
+                        Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+                    },
+                    label = {
+                        Text("ItemText")
+                    },
+                    selected = true,
+                    onClick = {}
+                )
             }
         }
 
         val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
         val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
             .getUnclippedBoundsInRoot()
-        val textBounds = rule.onNodeWithText("ItemText", useUnmergedTree = true)
-            .getUnclippedBoundsInRoot()
 
-        // Distance from the bottom of the item to the text bottom, and from the top of the icon to
-        // the top of the item
-        val verticalPadding = NavigationBarItemVerticalPadding
-
-        val itemBottom = itemBounds.height + itemBounds.top
-        // Text bottom should be `verticalPadding` from the bottom of the item
-        textBounds.bottom.assertIsEqualTo(itemBottom - verticalPadding)
+        // Distance from the top of the item to the top of the icon for the default height
+        val verticalPadding = 16.dp
 
         rule.onNodeWithTag("icon", useUnmergedTree = true)
-            // The icon should be centered in the item
+            // The icon should be horizontally centered in the item
             .assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
             // The top of the icon is `verticalPadding` below the top of the item
             .assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+        val iconBottom = iconBounds.top + iconBounds.height
+        // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+        // bottom of the icon
+        rule.onNodeWithText("ItemText", useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+            .top
+            .assertIsEqualTo(
+                iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+            )
     }
 
     @Test
@@ -367,6 +369,49 @@
     }
 
     @Test
+    fun navigationBarItemContent_customHeight_withLabel_sizeAndPosition() {
+        val defaultHeight = NavigationBarTokens.ContainerHeight
+        val customHeight = 64.dp
+
+        rule.setMaterialContent(lightColorScheme()) {
+            NavigationBar(Modifier.height(customHeight)) {
+                NavigationBarItem(
+                    modifier = Modifier.testTag("item"),
+                    icon = {
+                        Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+                    },
+                    label = { Text("Label") },
+                    selected = true,
+                    onClick = {}
+                )
+            }
+        }
+
+        // Vertical padding is removed symmetrically from top and bottom for smaller heights
+        val verticalPadding = 16.dp - (defaultHeight - customHeight) / 2
+
+        val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
+        val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+
+        rule.onNodeWithTag("icon", useUnmergedTree = true)
+            // The icon should be horizontally centered in the item
+            .assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
+            // The top of the icon is `verticalPadding` below the top of the item
+            .assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+        val iconBottom = iconBounds.top + iconBounds.height
+        // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+        // bottom of the item
+        rule.onNodeWithText("Label", useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+            .top
+            .assertIsEqualTo(
+                iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+            )
+    }
+
+    @Test
     fun navigationBar_selectNewItem() {
         rule.setMaterialContent(lightColorScheme()) {
             var selectedItem by remember { mutableStateOf(0) }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
index 0acd46a..5948252 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
@@ -75,7 +75,6 @@
  * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
  */
 @OptIn(ExperimentalMaterial3Api::class)
-@Suppress("ModifierParameter")
 @Deprecated(
     level = DeprecationLevel.HIDDEN,
     replaceWith = ReplaceWith(
@@ -147,7 +146,6 @@
  * @param properties [PopupProperties] for further customization of this popup's behavior
  * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
  */
-@Suppress("ModifierParameter")
 @Composable
 fun DropdownMenu(
     expanded: Boolean,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
index 318985d..4b558c6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -61,7 +61,6 @@
 import kotlin.math.max
 import kotlin.math.min
 
-@Suppress("ModifierParameter")
 @Composable
 internal fun DropdownMenuContent(
     expandedStates: MutableTransitionState<Boolean>,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 2746436..5f1bf664 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -535,8 +535,8 @@
  * [animationProgress].
  *
  * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
- * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
- * the spec.
+ * near the top of the item and the [labelPlaceable] will be placed beneath it with padding,
+ * according to the spec.
  *
  * When [animationProgress] is 1 (representing the selected state), the positions will be the same
  * as above.
@@ -573,11 +573,13 @@
 ): MeasureResult {
     val height = constraints.maxHeight
 
-    // Label should be `ItemVerticalPadding` from the bottom
-    val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx()
+    val contentTotalHeight = iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+        NavigationBarIndicatorToLabelPadding.roundToPx() + labelPlaceable.height
+    val contentVerticalPadding = ((height - contentTotalHeight) / 2)
+        .coerceAtLeast(IndicatorVerticalPadding.roundToPx())
 
-    // Icon (when selected) should be `ItemVerticalPadding` from the top
-    val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
+    // Icon (when selected) should be `contentVerticalPadding` from top
+    val selectedIconY = contentVerticalPadding
     val unselectedIconY =
         if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
 
@@ -588,6 +590,10 @@
     // animationProgress.
     val offset = (iconDistance * (1 - animationProgress)).roundToInt()
 
+    // Label should be fixed padding below icon
+    val labelY = selectedIconY + iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+        NavigationBarIndicatorToLabelPadding.roundToPx()
+
     val containerWidth = constraints.maxWidth
 
     val labelX = (containerWidth - labelPlaceable.width) / 2
@@ -626,12 +632,13 @@
 internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
 
 /*@VisibleForTesting*/
-internal val NavigationBarItemVerticalPadding: Dp = 16.dp
+internal val NavigationBarIndicatorToLabelPadding: Dp = 4.dp
 
 private val IndicatorHorizontalPadding: Dp =
     (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
 
-private val IndicatorVerticalPadding: Dp =
+/*@VisibleForTesting*/
+internal val IndicatorVerticalPadding: Dp =
     (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
 
 private val IndicatorVerticalOffset: Dp = 12.dp
\ No newline at end of file
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index b4d8073..d23376d 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -15,9 +15,8 @@
  */
 
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,77 +24,54 @@
     id("com.android.library")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /* When updating dependencies, make sure to make the an an analogous update in the
-            corresponding block below */
-        api project(":compose:runtime:runtime")
-        api "androidx.annotation:annotation:1.1.0"
-
-        implementation(libs.kotlinStdlib)
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.testCore)
-        testImplementation(libs.testRules)
-
-        androidTestImplementation projectOrArtifact(':compose:ui:ui')
-        androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
-        androidTestImplementation projectOrArtifact(":compose:test-utils")
-        androidTestImplementation "androidx.fragment:fragment:1.3.0"
-        androidTestImplementation projectOrArtifact(":activity:activity-compose")
-        androidTestImplementation(libs.testUiautomator)
-        androidTestImplementation(libs.testCore)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoCore)
-
-        lintPublish(project(":compose:runtime:runtime-saveable-lint"))
-
-        samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /* When updating dependencies, make sure to make the an an analogous update in the
-            corresponding block above */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
                 api project(":compose:runtime:runtime")
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 implementation(libs.kotlinStdlib)
                 api "androidx.annotation:annotation:1.1.0"
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.truth)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation project(':compose:ui:ui')
                 implementation project(":compose:ui:ui-test-junit4")
                 implementation project(":compose:test-utils")
@@ -112,10 +88,32 @@
                 implementation(libs.mockitoCore)
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
-    dependencies {
-        samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
-    }
+}
+
+dependencies {
+    samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
+    lintPublish(project(":compose:runtime:runtime-saveable-lint"))
 }
 
 androidx {
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 5b5d26c..46eecc3 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
 import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,89 +23,93 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = false // b/276387374 TODO: KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-
-        api("androidx.activity:activity:1.2.0")
-        api(projectOrArtifact(":compose:ui:ui-test-junit4"))
-        api(project(":test:screenshot:screenshot"))
-
-        implementation(libs.kotlinStdlibCommon)
-        implementation(projectOrArtifact(":compose:runtime:runtime"))
-        implementation(projectOrArtifact(":compose:ui:ui-unit"))
-        implementation(projectOrArtifact(":compose:ui:ui-graphics"))
-        implementation("androidx.activity:activity-compose:1.3.1")
-        // old version of common-java8 conflicts with newer version, because both have
-        // DefaultLifecycleEventObserver.
-        // Outside of androidx this is resolved via constraint added to lifecycle-common,
-        // but it doesn't work in androidx.
-        // See aosp/1804059
-        implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
-        implementation(libs.testCore)
-        implementation(libs.testRules)
-
-        // This has stub APIs for access to legacy Android APIs, so we don't want
-        // any dependency on this module.
-        compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
-
-        testImplementation(libs.truth)
-
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(projectOrArtifact(":compose:material:material"))
-    }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 implementation(projectOrArtifact(":compose:runtime:runtime"))
                 implementation(projectOrArtifact(":compose:ui:ui-unit"))
                 implementation(projectOrArtifact(":compose:ui:ui-graphics"))
                 implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
             }
+        }
+        androidMain.dependencies {
+            api("androidx.activity:activity:1.2.0")
+            implementation "androidx.activity:activity-compose:1.3.1"
+            api(projectOrArtifact(":compose:ui:ui-test-junit4"))
+            api(project(":test:screenshot:screenshot"))
+            // This has stub APIs for access to legacy Android APIs, so we don't want
+            // any dependency on this module.
+            compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+            implementation(libs.testCore)
+            implementation(libs.testRules)
+        }
 
-            androidMain.dependencies {
-                api("androidx.activity:activity:1.2.0")
-                implementation "androidx.activity:activity-compose:1.3.1"
-                api(projectOrArtifact(":compose:ui:ui-test-junit4"))
-                api(project(":test:screenshot:screenshot"))
-                // This has stub APIs for access to legacy Android APIs, so we don't want
-                // any dependency on this module.
-                compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
-                implementation(libs.testCore)
-                implementation(libs.testRules)
+        commonTest {
+            dependencies {
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.truth)
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
             }
+        }
 
-            androidAndroidTest.dependencies {
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.truth)
                 implementation(projectOrArtifact(":compose:material:material"))
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                }
+            }
+        }
     }
 }
 
diff --git a/compose/ui/ui-geometry/build.gradle b/compose/ui/ui-geometry/build.gradle
index 1a05daf..15e6e16 100644
--- a/compose/ui/ui-geometry/build.gradle
+++ b/compose/ui/ui-geometry/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,53 +23,69 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api("androidx.annotation:annotation:1.1.0")
-
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation(project(":compose:ui:ui-util"))
-        implementation(libs.kotlinStdlib)
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.kotlinTest)
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
-                implementation(project(":compose:runtime:runtime"))
+                implementation("androidx.compose.runtime:runtime:1.2.1")
                 implementation(project(":compose:ui:ui-util"))
             }
-            jvmMain.dependencies {
+        }
+
+        commonTest {
+            dependencies {
+                implementation(kotlin("test-junit"))
+            }
+        }
+
+        jvmMain {
+            dependencies {
                 implementation(libs.kotlinStdlib)
             }
-            androidMain.dependencies {
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
             }
-            commonTest.dependencies {
-                implementation(kotlin("test-junit"))
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(project(":compose:runtime:runtime"))
+                }
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        androidTest {
+            dependsOn(jvmTest)
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
             }
         }
     }
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
index 786ed1f..a1c9071 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
@@ -28,6 +28,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.math.roundToInt
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -197,6 +199,53 @@
         )
     }
 
+    @Test
+    fun testInvalidWidthBrush() {
+        // Verify that attempts to create a RadialGradient with a width of 0 do not throw
+        // IllegalArgumentExceptions for an invalid radius
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+        brush.applyTo(Size(0f, 10f), paint, 1.0f)
+    }
+
+    @Test
+    fun testInvalidHeightBrush() {
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+        // Verify that attempts to create a RadialGradient with a height of 0 do not throw
+        // IllegalArgumentExceptions for an invalid radius
+        brush.applyTo(Size(10f, 0f), paint, 1.0f)
+    }
+
+    @Test
+    fun testValidToInvalidWidthBrush() {
+        // Verify that attempts to create a RadialGradient with a non-zero width/height that
+        // is later attempted to be recreated with a zero width remove the shader from the Paint
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+        brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+        assertNotNull(paint.shader)
+
+        brush.applyTo(Size(0f, 10f), paint, 1.0f)
+        assertNull(paint.shader)
+    }
+
+    @Test
+    fun testValidToInvalidHeightBrush() {
+        // Verify that attempts to create a RadialGradient with a non-zero width/height that
+        // is later attempted to be recreated with a zero height remove the shader from the Paint
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+
+        brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+        assertNotNull(paint.shader)
+
+        brush.applyTo(Size(10f, 0f), paint, 1.0f)
+        assertNull(paint.shader)
+    }
+
     private fun ImageBitmap.drawInto(
         block: DrawScope.() -> Unit
     ) = CanvasDrawScope().draw(
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
index a18e8db..5f7a1d0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
@@ -653,8 +653,14 @@
     final override fun applyTo(size: Size, p: Paint, alpha: Float) {
         var shader = internalShader
         if (shader == null || createdSize != size) {
-            shader = createShader(size).also { internalShader = it }
-            createdSize = size
+            if (size.isEmpty()) {
+                shader = null
+                internalShader = null
+                createdSize = Size.Unspecified
+            } else {
+                shader = createShader(size).also { internalShader = it }
+                createdSize = size
+            }
         }
         if (p.color != Color.Black) p.color = Color.Black
         if (p.shader != shader) p.shader = shader
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 386b478..1aef72f 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1348,7 +1348,7 @@
         val paragraph = simpleParagraph(
             text = "",
             style = TextStyle(brush = brush),
-            width = 0.0f
+            width = 1.0f
         )
 
         assertThat(paragraph.textPaint.shader).isNotNull()
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index a9f9236..d6d3a42 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,47 +23,91 @@
     id("com.android.library")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
 
-dependencies {
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        implementation(libs.kotlinStdlib)
-        api("androidx.annotation:annotation:1.2.0")
-        api("androidx.compose.runtime:runtime:1.2.1")
-        testImplementation(libs.junit)
-    }
-}
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
 
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:runtime:runtime"))
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    api(project(":compose:runtime:runtime"))
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.2.0")
             }
+        }
 
-            androidTest.dependencies {
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.junit)
             }
         }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+
+                }
+            }
+        }
     }
 }
 
-
 androidx {
     name = "Compose Tooling API"
     type = LibraryType.PUBLISHED_LIBRARY
diff --git a/compose/ui/ui-unit/build.gradle b/compose/ui/ui-unit/build.gradle
index bfbfed7..627cd87 100644
--- a/compose/ui/ui-unit/build.gradle
+++ b/compose/ui/ui-unit/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,85 +23,88 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api(project(":compose:ui:ui-geometry"))
-        api("androidx.annotation:annotation:1.1.0")
-
-        implementation(libs.kotlinStdlib)
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation(project(":compose:ui:ui-util"))
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testExtJunit)
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.kotlinTest)
-
-        samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:ui:ui-geometry"))
 
                 implementation(project(":compose:runtime:runtime"))
                 implementation(project(":compose:ui:ui-util"))
             }
-            jvmMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-            androidMain.dependencies {
-                api("androidx.annotation:annotation:1.1.0")
-            }
+        }
 
-            commonTest.dependencies {
+        commonTest {
+            dependencies {
                 implementation(kotlin("test-junit"))
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.truth)
+        jvmMain {
+            dependencies {
+                implementation(libs.kotlinStdlib)
             }
-            androidAndroidTest.dependencies {
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api("androidx.annotation:annotation:1.1.0")
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(project(":compose:runtime:runtime"))
+                }
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.testExtJunit)
                 implementation(libs.espressoCore)
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
-    dependencies {
-        samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
-    }
+}
+
+dependencies {
+    samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
 }
 
 androidx {
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index 8eeb75e..4c19723 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,59 +23,72 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        implementation(libs.kotlinStdlib)
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.kotlinTest)
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
             }
+        }
 
-            jvmMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-
-            androidMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-
-            commonTest.dependencies {
+        commonTest {
+            dependencies {
                 implementation(kotlin("test-junit"))
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
+        jvmMain {
+            dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.truth)
             }
         }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
 }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
index 32fd60c..f761c28 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
@@ -4405,38 +4405,3 @@
 )
 
 internal typealias PointerEventHandler = (PointerEvent, PointerEventPass, IntSize) -> Unit
-
-private fun PointerEventHandler.invokeOverAllPasses(
-    pointerEvent: PointerEvent,
-    size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
-    invokeOverPasses(
-        pointerEvent,
-        listOf(
-            PointerEventPass.Initial,
-            PointerEventPass.Main,
-            PointerEventPass.Final
-        ),
-        size = size
-    )
-}
-
-private fun PointerEventHandler.invokeOverPasses(
-    pointerEvent: PointerEvent,
-    vararg pointerEventPasses: PointerEventPass,
-    size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
-    invokeOverPasses(pointerEvent, pointerEventPasses.toList(), size)
-}
-
-private fun PointerEventHandler.invokeOverPasses(
-    pointerEvent: PointerEvent,
-    pointerEventPasses: List<PointerEventPass>,
-    size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
-    require(pointerEvent.changes.isNotEmpty())
-    require(pointerEventPasses.isNotEmpty())
-    pointerEventPasses.forEach {
-        this.invoke(pointerEvent, it, size)
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 497be6e..da63070 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -911,13 +911,20 @@
         }
     }
 
-    private fun convertMeasureSpec(measureSpec: Int): Pair<Int, Int> {
+    @Suppress("NOTHING_TO_INLINE")
+    private inline operator fun ULong.component1() = (this shr 32).toInt()
+    @Suppress("NOTHING_TO_INLINE")
+    private inline operator fun ULong.component2() = (this and 0xFFFFFFFFUL).toInt()
+
+    private fun pack(a: Int, b: Int) = (a.toULong() shl 32 or b.toULong())
+
+    private fun convertMeasureSpec(measureSpec: Int): ULong {
         val mode = MeasureSpec.getMode(measureSpec)
         val size = MeasureSpec.getSize(measureSpec)
         return when (mode) {
-            MeasureSpec.EXACTLY -> size to size
-            MeasureSpec.UNSPECIFIED -> 0 to Constraints.Infinity
-            MeasureSpec.AT_MOST -> 0 to size
+            MeasureSpec.EXACTLY -> pack(size, size)
+            MeasureSpec.UNSPECIFIED -> pack(0, Constraints.Infinity)
+            MeasureSpec.AT_MOST -> pack(0, size)
             else -> throw IllegalStateException()
         }
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index 9a7ddac..abd9b6a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -376,12 +376,21 @@
 
         @OptIn(ExperimentalComposeUiApi::class)
         for ((key, change) in changes) {
-            // Filter for changes that are associated with pointer ids that are relevant to this
-            // node
-            if (key in pointerIds) {
+            val keyValue = key.value
+
+            // Using for (key in pointerIds) causes key to be boxed and create allocations
+            var keyInPointerIds = false
+            for (i in 0..pointerIds.lastIndex) {
+                if (pointerIds[i].value == keyValue) {
+                    keyInPointerIds = true
+                    break
+                }
+            }
+
+            if (keyInPointerIds) {
                 // And translate their position relative to the parent coordinates, to give us a
                 // change local to the PointerInputFilter's coordinates
-                val historical = mutableListOf<HistoricalChange>()
+                val historical = ArrayList<HistoricalChange>(change.historical.size)
                 change.historical.fastForEach {
                     historical.add(
                         HistoricalChange(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index c9f3e51..aa215e9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -66,6 +66,8 @@
  */
 private const val DebugChanges = false
 
+private val DefaultDensity = Density(1f)
+
 /**
  * An element in the layout hierarchy, built with compose UI.
  */
@@ -627,7 +629,7 @@
     /**
      * The screen density to be used by this layout.
      */
-    override var density: Density = Density(1f)
+    override var density: Density = DefaultDensity
         set(value) {
             if (field != value) {
                 field = value
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index 2b2847f..9043a07 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -743,7 +743,8 @@
 private fun Modifier.fillVector(
     result: MutableVector<Modifier.Element>
 ): MutableVector<Modifier.Element> {
-    val stack = MutableVector<Modifier>(result.size).also { it.add(this) }
+    val capacity = result.size.coerceAtLeast(16)
+    val stack = MutableVector<Modifier>(capacity).also { it.add(this) }
     while (stack.isNotEmpty()) {
         when (val next = stack.removeAt(stack.size - 1)) {
             is CombinedModifier -> {
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 639aa60..51a3271 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -14,9 +14,7 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
@@ -24,77 +22,49 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+androidXMultiplatform {
+    android()
 
-dependencies {
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        implementation(project(":compose:ui:ui"))
-        implementation(project(":compose:ui:ui-unit"))
-        implementation(project(":compose:ui:ui-util"))
-        implementation(project(":compose:foundation:foundation"))
-        implementation(project(":compose:foundation:foundation-layout"))
-
-        implementation(project(":constraintlayout:constraintlayout-core"))
-
-        androidTestImplementation(project(":compose:material:material"))
-        androidTestImplementation(project(":compose:ui:ui-test"))
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(project(":compose:ui:ui-test-manifest"))
-        androidTestImplementation(project(":activity:activity"))
-
-        androidTestImplementation(libs.kotlinTest)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-
-        lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
-//                implementation(libs.kotlinStdlibCommon)
-
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(project(":compose:ui:ui"))
-                implementation("androidx.compose.ui:ui-unit:1.4.0-beta02")
-                implementation("androidx.compose.ui:ui-util:1.4.0-beta02")
-                implementation("androidx.compose.foundation:foundation:1.4.0-beta02")
-                implementation("androidx.compose.foundation:foundation-layout:1.4.0-beta02")
+                implementation(project(":compose:ui:ui-unit"))
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":compose:foundation:foundation"))
+                implementation(project(":compose:foundation:foundation-layout"))
                 implementation(project(":constraintlayout:constraintlayout-core"))
-
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(commonMain)
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
                 implementation("androidx.core:core-ktx:1.5.0")
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        jvmTest {
+            dependencies {
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-            }
-
-            androidAndroidTest.dependencies {
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.kotlinTest)
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
@@ -107,9 +77,27 @@
                 implementation(project(":compose:test-utils"))
             }
         }
+
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+            }
+        }
     }
 }
 
+dependencies {
+    lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
+}
+
 androidx {
     name = "Android ConstraintLayout Compose Library"
     type = LibraryType.PUBLISHED_LIBRARY
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
index 00afeb8..a4432f0 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
@@ -162,13 +162,23 @@
         @Override
         public void reportResult(String result) {
             Objects.requireNonNull(result);
-            handleEvaluationResult(mCompleter, result);
+            final long identityToken = Binder.clearCallingIdentity();
+            try {
+                handleEvaluationResult(mCompleter, result);
+            } finally {
+                Binder.restoreCallingIdentity(identityToken);
+            }
         }
 
         @Override
         public void reportError(@ExecutionErrorTypes int type, String error) {
             Objects.requireNonNull(error);
-            handleEvaluationError(mCompleter, type, error);
+            final long identityToken = Binder.clearCallingIdentity();
+            try {
+                handleEvaluationError(mCompleter, type, error);
+            } finally {
+                Binder.restoreCallingIdentity(identityToken);
+            }
         }
     }
 
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
index 083d17d..e820bfe 100644
--- a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.Color
 import android.os.Bundle
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -27,6 +28,7 @@
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.FragmentNavigatorExtras
 import androidx.navigation.fragment.findNavController
+import androidx.transition.Slide
 
 /**
  * Fragment used to show how to navigate to another destination
@@ -38,6 +40,8 @@
         container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View? {
+        enterTransition = Slide(Gravity.RIGHT)
+        exitTransition = Slide(Gravity.LEFT)
         return inflater.inflate(R.layout.main_fragment, container, false)
     }
 
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
index cea98bd..4db4d1d 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
@@ -23,31 +23,19 @@
                   android:name=".MainFragment"
                   android:label="@string/home">
             <argument android:name="myarg" android:defaultValue="Home" />
-            <action android:id="@+id/next" app:destination="@+id/first_screen"
-                app:enterAnim="@anim/nav_default_enter_anim"
-                app:exitAnim="@anim/nav_default_exit_anim"
-                app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-                app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+            <action android:id="@+id/next" app:destination="@+id/first_screen"/>
         </fragment>
         <fragment android:id="@+id/first_screen"
             android:name="androidx.navigation.testapp.MainFragment"
             android:label="@string/first">
             <argument android:name="myarg" android:defaultValue="one" />
-            <action android:id="@+id/next" app:destination="@+id/next_fragment"
-                app:enterAnim="@anim/nav_default_enter_anim"
-                app:exitAnim="@anim/nav_default_exit_anim"
-                app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-                app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+            <action android:id="@+id/next" app:destination="@+id/next_fragment"/>
         </fragment>
         <fragment android:id="@+id/next_fragment"
             android:name="androidx.navigation.testapp.MainFragment"
             android:label="@string/second">
             <argument android:name="myarg" android:defaultValue="two" />
-            <action android:id="@+id/next" app:destination="@+id/first_screen"
-                app:enterAnim="@anim/nav_default_enter_anim"
-                app:exitAnim="@anim/nav_default_exit_anim"
-                app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-                app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+            <action android:id="@+id/next" app:destination="@+id/first_screen"/>
         </fragment>
     </navigation>
     <dialog
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
index f7eb4da..63261cbf 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
@@ -20,51 +20,31 @@
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/first">
         <argument android:name="myarg" android:defaultValue="one" />
-        <action android:id="@+id/next" app:destination="@+id/second_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/second_fragment"/>
     </fragment>
     <fragment android:id="@+id/second_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/second">
         <argument android:name="myarg" android:defaultValue="two" />
-        <action android:id="@+id/next" app:destination="@+id/third_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/third_fragment"/>
     </fragment>
     <fragment android:id="@+id/third_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/third">
         <argument android:name="myarg" android:defaultValue="three" />
-        <action android:id="@+id/next" app:destination="@+id/fourth_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/fourth_fragment"/>
     </fragment>
     <fragment android:id="@+id/fourth_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/fourth">
         <argument android:name="myarg" android:defaultValue="four" />
-        <action android:id="@+id/next" app:destination="@+id/fifth_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/fifth_fragment"/>
     </fragment>
     <fragment android:id="@+id/fifth_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/fifth">
         <argument android:name="myarg" android:defaultValue="five" />
-        <action android:id="@+id/next" app:destination="@+id/first_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/first_fragment"/>
     </fragment>
     <dialog
         android:id="@+id/learn_more"
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -46,7 +46,7 @@
     method @AnyThread public void onInvalidated();
   }
 
-  public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+  public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
     ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public void invalidate();
     method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
   public static final class PagingSource.LoadResult.Page.Companion {
   }
 
+  public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+    method public operator androidx.paging.PagingSource<Key,Value> invoke();
+  }
+
   public final class PagingState<Key, Value> {
     ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
     method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 07e6471..a5d3d01 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -49,7 +49,7 @@
   @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalPagingApi {
   }
 
-  public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+  public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
     ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public void invalidate();
     method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -415,6 +415,10 @@
   public static final class PagingSource.LoadResult.Page.Companion {
   }
 
+  public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+    method public operator androidx.paging.PagingSource<Key,Value> invoke();
+  }
+
   public final class PagingState<Key, Value> {
     ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
     method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -46,7 +46,7 @@
     method @AnyThread public void onInvalidated();
   }
 
-  public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+  public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
     ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public void invalidate();
     method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
   public static final class PagingSource.LoadResult.Page.Companion {
   }
 
+  public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+    method public operator androidx.paging.PagingSource<Key,Value> invoke();
+  }
+
   public final class PagingState<Key, Value> {
     ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
     method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
index 93ddb2d..3f74d54 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
@@ -32,7 +32,7 @@
  */
 public class InvalidatingPagingSourceFactory<Key : Any, Value : Any>(
     private val pagingSourceFactory: () -> PagingSource<Key, Value>
-) : () -> PagingSource<Key, Value> {
+) : PagingSourceFactory<Key, Value> {
 
     @VisibleForTesting
     internal val pagingSources = CopyOnWriteArrayList<PagingSource<Key, Value>>()
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
new file mode 100644
index 0000000..4ed3c7f
--- /dev/null
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 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
+
+/**
+ * Interface for a factory that generates [PagingSource].
+ *
+ * The factory extending this interface can be used to instantiate a [Pager] as the
+ * pagingSourceFactory.
+ */
+public fun interface PagingSourceFactory<Key : Any, Value : Any> : () -> PagingSource<Key, Value> {
+    /**
+     * Returns a new PagingSource instance.
+     *
+     * This function can be invoked by calling pagingSourceFactory() or pagingSourceFactory::invoke.
+     */
+    public override operator fun invoke(): PagingSource<Key, Value>
+}
\ No newline at end of file
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -26,7 +26,7 @@
   }
 
   public final class StaticListPagingSourceFactoryKt {
-    method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+    method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
 
   public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -26,7 +26,7 @@
   }
 
   public final class StaticListPagingSourceFactoryKt {
-    method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+    method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
 
   public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -26,7 +26,7 @@
   }
 
   public final class StaticListPagingSourceFactoryKt {
-    method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+    method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
 
   public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
index e44216e..3cf9165 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
@@ -19,6 +19,7 @@
 import androidx.paging.InvalidatingPagingSourceFactory
 import androidx.paging.PagingSource
 import androidx.paging.Pager
+import androidx.paging.PagingSourceFactory
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.launch
@@ -41,7 +42,7 @@
  */
 public fun <Value : Any> Flow<@JvmSuppressWildcards List<Value>>.asPagingSourceFactory(
     coroutineScope: CoroutineScope
-): () -> PagingSource<Int, Value> {
+): PagingSourceFactory<Int, Value> {
 
     var data: List<Value>? = null
 
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index 42aaa0b5..312d95b 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -20,6 +20,7 @@
 import androidx.paging.PagingConfig
 import androidx.paging.PagingSource
 import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSourceFactory
 import androidx.paging.PagingState
 import androidx.paging.cachedIn
 import androidx.paging.insertSeparators
@@ -2355,9 +2356,9 @@
 }
 
 private class WrappedPagingSourceFactory(
-    private val factory: () -> PagingSource<Int, Int>,
+    private val factory: PagingSourceFactory<Int, Int>,
     private val loadDelay: Long,
-) : () -> PagingSource<Int, Int> {
+) : PagingSourceFactory<Int, Int> {
     override fun invoke(): PagingSource<Int, Int> = TestPagingSource(factory(), loadDelay)
 }
 
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
index 6a2f2c1..eb28936 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
@@ -17,8 +17,8 @@
 package androidx.paging.testing
 
 import androidx.paging.PagingConfig
-import androidx.paging.PagingSource
 import androidx.paging.PagingSource.LoadResult.Page
+import androidx.paging.PagingSourceFactory
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
@@ -47,7 +47,7 @@
 
     @Test
     fun emptyFlow() {
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             flowOf<List<Int>>().asPagingSourceFactory(testScope)
         val pagingSource = factory()
         val pager = TestPager(pagingSource, CONFIG)
@@ -64,7 +64,7 @@
             List(20) { it }
         )
 
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             flow.asPagingSourceFactory(testScope)
         val pagingSource = factory()
         val pager = TestPager(pagingSource, CONFIG)
@@ -85,7 +85,7 @@
             emit(List(15) { it + 30 }) // second gen
         }
 
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             flow.asPagingSourceFactory(testScope)
 
         advanceTimeBy(1000)
@@ -117,7 +117,7 @@
         val mutableFlow = MutableSharedFlow<List<Int>>()
         val collectionScope = this.backgroundScope
 
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             mutableFlow.asPagingSourceFactory(collectionScope)
 
         mutableFlow.emit(List(10) { it })
@@ -146,10 +146,10 @@
     fun multipleFactories_fromSameFlow() = testScope.runTest {
         val mutableFlow = MutableSharedFlow<List<Int>>()
 
-        val factory1: () -> PagingSource<Int, Int> =
+        val factory1: PagingSourceFactory<Int, Int> =
             mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
 
-        val factory2: () -> PagingSource<Int, Int> =
+        val factory2: PagingSourceFactory<Int, Int> =
             mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
 
         mutableFlow.emit(List(10) { it })
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 470c9af..213eeb9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -22,10 +22,10 @@
 import com.google.devtools.ksp.symbol.KSDeclaration
 import com.google.devtools.ksp.symbol.KSNode
 import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeAlias
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
 import com.google.devtools.ksp.symbol.KSTypeReference
-import com.google.devtools.ksp.symbol.Modifier
 
 /**
  * Root package comes as <root> instead of "" so we work around it here.
@@ -47,10 +47,12 @@
 }
 
 internal fun KSTypeReference.isTypeParameterReference(): Boolean {
-    return this.resolve().declaration is KSTypeParameter
+    return this.resolve().isTypeParameter()
 }
 
-fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
+internal fun KSType.isTypeParameter(): Boolean {
+    return declaration is KSTypeParameter
+}
 
 internal fun KSType.withNullability(nullability: XNullability) = when (nullability) {
     XNullability.NULLABLE -> makeNullable()
@@ -59,14 +61,15 @@
 }
 
 private fun KSAnnotated.hasAnnotation(qName: String) =
-    annotations.any { it.hasQualifiedName(qName) }
+    annotations.any { it.hasQualifiedNameOrAlias(qName) }
 
-private fun KSAnnotation.hasQualifiedName(qName: String): Boolean {
-    return annotationType.resolve().hasQualifiedName(qName)
+private fun KSAnnotation.hasQualifiedNameOrAlias(qName: String): Boolean {
+    return annotationType.resolve().hasQualifiedNameOrAlias(qName)
 }
 
-private fun KSType.hasQualifiedName(qName: String): Boolean {
-    return declaration.qualifiedName?.asString() == qName
+private fun KSType.hasQualifiedNameOrAlias(qName: String): Boolean {
+    return declaration.qualifiedName?.asString() == qName ||
+        (declaration as? KSTypeAlias)?.type?.resolve()?.hasQualifiedNameOrAlias(qName) ?: false
 }
 
 internal fun KSAnnotated.hasJvmWildcardAnnotation() =
@@ -75,20 +78,55 @@
 internal fun KSAnnotated.hasSuppressJvmWildcardAnnotation() =
     hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
 
-private fun KSType.hasAnnotation(qName: String) = annotations.any { it.hasQualifiedName(qName) }
-
-internal fun KSType.hasJvmWildcardAnnotation() =
-    hasAnnotation(JvmWildcard::class.java.canonicalName!!)
+// TODO(bcorso): There's a bug in KSP where, after using KSType#asMemberOf() or KSType#replace(),
+//  the annotations are removed from the resulting type. However, it turns out that the annotation
+//  information is still available in the underlying KotlinType, so we use reflection to get them.
+//  See https://github.com/google/ksp/issues/1376.
+private fun KSType.hasAnnotation(qName: String): Boolean {
+    fun String.toFqName(): Any {
+        return Class.forName("org.jetbrains.kotlin.name.FqName")
+            .getConstructor(String::class.java)
+            .newInstance(this)
+    }
+    fun hasAnnotationViaReflection(qName: String): Boolean {
+        val ksType = if (
+            // Note: Technically, we could just make KSTypeWrapper internal and cast to get the
+            // delegate, but since we need to use reflection anyway, just get it via reflection.
+            this.javaClass.canonicalName == "androidx.room.compiler.processing.ksp.KSTypeWrapper") {
+            this.javaClass.methods.find { it.name == "getDelegate" }?.invoke(this)
+        } else {
+            this
+        }
+        val kotlinType =
+            ksType?.javaClass?.methods?.find { it.name == "getKotlinType" }?.invoke(ksType)
+        val kotlinAnnotations =
+            kotlinType?.javaClass
+                ?.methods
+                ?.find { it.name == "getAnnotations" }
+                ?.invoke(kotlinType)
+        return kotlinAnnotations?.javaClass
+            ?.methods
+            ?.find { it.name == "hasAnnotation" }
+            ?.invoke(kotlinAnnotations, qName.toFqName()) == true
+    }
+    return if (annotations.toList().isEmpty()) {
+        // If there are no annotations but KSType#toString() shows annotations, check the underlying
+        // KotlinType for annotations using reflection.
+        toString().startsWith("[") && hasAnnotationViaReflection(qName)
+    } else {
+        annotations.any { it.annotationType.resolve().hasQualifiedNameOrAlias(qName) }
+    }
+}
 
 internal fun KSType.hasSuppressJvmWildcardAnnotation() =
     hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
 
  internal fun KSNode.hasSuppressWildcardsAnnotationInHierarchy(): Boolean {
-     (this as? KSAnnotated)?.let {
-         if (hasSuppressJvmWildcardAnnotation()) {
-             return true
-         }
-     }
-     val parent = parent ?: return false
-     return parent.hasSuppressWildcardsAnnotationInHierarchy()
- }
\ No newline at end of file
+    (this as? KSAnnotated)?.let {
+        if (hasSuppressJvmWildcardAnnotation()) {
+            return true
+        }
+    }
+    val parent = parent ?: return false
+    return parent.hasSuppressWildcardsAnnotationInHierarchy()
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index bdade61..d797d50 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -25,6 +25,7 @@
 import com.google.devtools.ksp.symbol.KSTypeAlias
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
 import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Variance
 
@@ -59,21 +60,57 @@
             return type
         }
 
-        val resolvedType = if (hasTypeVariables(scope.declarationType())) {
+        // First, replace any type aliases in the type with their actual types
+        return type.replaceTypeAliases()
+            // Next, resolve wildcards based on the scope of the type
+            .resolveWildcards(scope)
+            // Next, apply any additional variance changes based on the @JvmSuppressWildcards or
+            // @JvmWildcard annotations on the resolved type.
+            .applyJvmWildcardAnnotations()
+            // Finally, unwrap any delegate types. (Note: as part of resolving wildcards, we wrap
+            // types/type arguments in delegates to avoid loosing annotation information. However,
+            // those delegates may cause issues later if KSP tries to cast the type/argument to a
+            // particular implementation, so we unwrap them here.
+            .removeWrappers()
+    }
+
+    private fun KSType.replaceTypeAliases(typeStack: ReferenceStack = ReferenceStack()): KSType {
+        if (isError || typeStack.queue.contains(this)) {
+            return this
+        }
+        if (declaration is KSTypeAlias) {
+            return (declaration as KSTypeAlias).type.resolve().replaceTypeAliases(typeStack)
+        }
+        return typeStack.withReference(this) {
+            createWrapper(arguments.map { it.replaceTypeAliases(typeStack) })
+        }
+    }
+
+    private fun KSTypeArgument.replaceTypeAliases(typeStack: ReferenceStack): KSTypeArgument {
+        val type = type?.resolve()
+        if (
+            type == null ||
+            type.isError ||
+            variance == Variance.STAR ||
+            typeStack.queue.contains(type)
+        ) {
+            return this
+        }
+        return createWrapper(type.replaceTypeAliases(typeStack), variance)
+    }
+
+    private fun KSType.resolveWildcards(scope: KSTypeVarianceResolverScope): KSType {
+        return if (hasTypeVariables(scope.declarationType())) {
             // If the associated declared type contains type variables that were resolved, e.g.
             // using "asMemberOf", then it has special rules about how to resolve the types.
             getJavaWildcardWithTypeVariables(
-                type = type,
+                type = this,
                 declarationType = getJavaWildcard(scope.declarationType(), scope),
                 scope = scope,
             )
         } else {
-            getJavaWildcard(type, scope)
+            getJavaWildcard(this, scope)
         }
-
-        // As a final pass, we apply variance from any @JvmSuppressWildcards or @JvmWildcard
-        // annotations on the resolved type.
-        return applyJvmWildcardAnnotations(resolvedType)
     }
 
     private fun hasTypeVariables(
@@ -99,15 +136,6 @@
         if (type.isError || typeStack.queue.contains(type)) {
             return type
         }
-        if (type.declaration is KSTypeAlias) {
-            return getJavaWildcard(
-                type = (type.declaration as KSTypeAlias).type.resolve(),
-                scope = scope,
-                typeStack = typeStack,
-                typeArgStack = typeArgStack,
-                typeParamStack = typeParamStack,
-            )
-        }
         return typeStack.withReference(type) {
             val resolvedTypeArgs =
                 type.arguments.indices.map { i ->
@@ -120,7 +148,7 @@
                         typeParamStack = typeParamStack,
                     )
                 }
-            type.replace(resolvedTypeArgs)
+            type.createWrapper(resolvedTypeArgs)
         }
     }
 
@@ -158,10 +186,10 @@
                 if (typeParamStack.indices.none { i ->
                         (typeParamStack[i].variance == Variance.CONTRAVARIANT ||
                             typeArgStack[i].variance == Variance.CONTRAVARIANT) &&
-                        // The declaration and use site variance is ignored when using @JvmWildcard
-                        // explicitly on a type.
-                        !typeArgStack[i].hasJvmWildcardAnnotation()
-                }) {
+                            // The declaration and use site variance is ignored when using @JvmWildcard
+                            // explicitly on a type.
+                            !typeArgStack[i].hasJvmWildcardAnnotation()
+                    }) {
                     return false
                 }
             } else {
@@ -217,7 +245,7 @@
         } else {
             typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
     private fun getJavaWildcardWithTypeVariables(
@@ -252,7 +280,7 @@
                         )
                     }
                 }
-            type.replace(resolvedTypeArgs)
+            type.createWrapper(resolvedTypeArgs)
         }
     }
 
@@ -293,7 +321,7 @@
         } else {
             typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
     private fun getJavaWildcardWithTypeVariablesForOuterType(
@@ -322,26 +350,27 @@
         } else {
             typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
-    private fun applyJvmWildcardAnnotations(
-        type: KSType,
+    private fun KSType.applyJvmWildcardAnnotations(
         typeStack: ReferenceStack = ReferenceStack(),
+        typeArgStack: List<KSTypeArgument> = emptyList(),
     ): KSType {
-        if (type.isError || typeStack.queue.contains(type)) {
-            return type
+        if (isError || typeStack.queue.contains(this)) {
+            return this
         }
-        return typeStack.withReference(type) {
+        return typeStack.withReference(this) {
             val resolvedTypeArgs =
-                type.arguments.indices.map { i ->
+                arguments.indices.map { i ->
                     applyJvmWildcardAnnotations(
-                        typeArg = type.arguments[i],
-                        typeParameter = type.declaration.typeParameters[i],
+                        typeArg = arguments[i],
+                        typeParameter = declaration.typeParameters[i],
+                        typeArgStack = typeArgStack,
                         typeStack = typeStack,
                     )
                 }
-            type.replace(resolvedTypeArgs)
+            createWrapper(resolvedTypeArgs)
         }
     }
 
@@ -349,6 +378,7 @@
         typeArg: KSTypeArgument,
         typeParameter: KSTypeParameter,
         typeStack: ReferenceStack,
+        typeArgStack: List<KSTypeArgument>,
     ): KSTypeArgument {
         val type = typeArg.type?.resolve()
         if (
@@ -359,28 +389,107 @@
         ) {
             return typeArg
         }
-        val resolvedType = applyJvmWildcardAnnotations(
-            type = type,
-            typeStack = typeStack,
-        )
+        val resolvedType = type.applyJvmWildcardAnnotations(typeStack, typeArgStack + typeArg)
         val resolvedVariance = when {
             typeParameter.variance == Variance.INVARIANT &&
                 typeArg.variance != Variance.INVARIANT -> typeArg.variance
             typeArg.hasJvmWildcardAnnotation() -> typeParameter.variance
-                typeStack.queue.any { it.hasSuppressJvmWildcardAnnotation() } ||
+            // We only need to check the first type in the stack for @JvmSuppressWildcards.
+            // Any other @JvmSuppressWildcards usages will be placed on the type arguments rather
+            // than the types, so no need to check the rest of the types.
+            typeStack.queue.first().hasSuppressJvmWildcardAnnotation() ||
                 typeArg.hasSuppressWildcardsAnnotationInHierarchy() ||
+                typeArgStack.any { it.hasSuppressJvmWildcardAnnotation() } ||
                 typeParameter.hasSuppressWildcardsAnnotationInHierarchy() -> Variance.INVARIANT
             else -> typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
-    private fun KSType.isTypeParameter(): Boolean {
-        return createTypeReference().isTypeParameterReference()
+    private fun KSTypeArgument.createWrapper(
+        newType: KSType,
+        newVariance: Variance
+    ): KSTypeArgument {
+        return KSTypeArgumentWrapper(
+            delegate = (this as? KSTypeArgumentWrapper)?.delegate ?: this,
+            type = newType.createTypeReference(),
+            variance = newVariance
+        )
     }
 
-    private fun createTypeArgument(type: KSType, variance: Variance): KSTypeArgument {
-        return resolver.getTypeArgument(type.createTypeReference(), variance)
+    private fun KSType.createWrapper(newArguments: List<KSTypeArgument>): KSType {
+        return KSTypeWrapper(
+            delegate = (this as? KSTypeWrapper)?.delegate ?: this,
+            arguments = newArguments
+        )
+    }
+
+    private fun KSType.removeWrappers(typeStack: ReferenceStack = ReferenceStack()): KSType {
+        if (isError || typeStack.queue.contains(this)) {
+            return this
+        }
+        return typeStack.withReference(this) {
+            val delegateType = (this as? KSTypeWrapper)?.delegate ?: this
+            delegateType.replace(arguments.map { it.removeWrappers(typeStack) })
+        }
+    }
+
+    private fun KSTypeArgument.removeWrappers(
+        typeStack: ReferenceStack = ReferenceStack()
+    ): KSTypeArgument {
+        val type = type?.resolve()
+        if (
+            type == null ||
+            type.isError ||
+            variance == Variance.STAR ||
+            typeStack.queue.contains(type)
+        ) {
+            return this
+        }
+        return resolver.getTypeArgument(
+            type.removeWrappers(typeStack).createTypeReference(),
+            variance
+        )
+    }
+}
+
+/**
+ * A wrapper for creating a new [KSType] that allows arguments of type [KSTypeArgumentWrapper].
+ *
+ * Note: This wrapper acts similar to [KSType#replace(KSTypeArgument)]. However, we can't call
+ * [KSType#replace(KSTypeArgument)] directly when using [KSTypeArgumentWrapper] or we'll get an
+ * [IllegalStateException] since KSP tries to cast to its own implementation of [KSTypeArgument].
+ */
+private class KSTypeWrapper(
+    val delegate: KSType,
+    override val arguments: List<KSTypeArgument>
+) : KSType by delegate {
+    override fun toString() = if (arguments.isNotEmpty()) {
+        "${delegate.toString().substringBefore("<")}<${arguments.joinToString(",")}>"
+    } else {
+        delegate.toString()
+    }
+}
+
+/**
+ * A wrapper for creating a new [KSTypeArgument] that delegates to the original argument for
+ * annotations.
+ *
+ * Note: This wrapper acts similar to [Resolver#getTypeArgument(KSTypeReference, Variance)].
+ * However, we can't call [Resolver#getTypeArgument(KSTypeReference, Variance)] directly because
+ * we'll lose information about annotations (e.g. `@JvmSuppressWildcards`) that were on the original
+ * type argument.
+ */
+private class KSTypeArgumentWrapper(
+    val delegate: KSTypeArgument,
+    override val type: KSTypeReference,
+    override val variance: Variance,
+) : KSTypeArgument by delegate {
+    override fun toString() = when (variance) {
+        Variance.INVARIANT -> "${type.resolve()}"
+        Variance.CONTRAVARIANT -> "in ${type.resolve()}"
+        Variance.COVARIANT -> "out ${type.resolve()}"
+        Variance.STAR -> "*"
     }
 }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
index f39ab11..19f546a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
@@ -27,7 +27,7 @@
  * Provides KSType resolution scope for a type.
  */
 internal sealed class KSTypeVarianceResolverScope(
-    private val annotated: KSAnnotated,
+    val annotated: KSAnnotated,
     private val container: KSDeclaration?,
     private val asMemberOf: KspType?
 ) {
@@ -36,8 +36,15 @@
      * parameter is in kotlin or the containing class, which inherited the method, is in kotlin.
      */
     val needsWildcardResolution: Boolean by lazy {
+        fun nodeForSuppressionCheck(): KSAnnotated? = when (this) {
+            // For property setter and getter methods skip to the enclosing class to check for
+            // suppression annotations to match KAPT.
+            is PropertySetterParameterType,
+            is PropertyGetterMethodReturnType -> annotated.parent?.parent as? KSAnnotated
+            else -> annotated
+        }
         (annotated.isInKotlinCode() || container?.isInKotlinCode() == true) &&
-            !annotated.hasSuppressWildcardsAnnotationInHierarchy()
+            nodeForSuppressionCheck()?.hasSuppressWildcardsAnnotationInHierarchy() != true
     }
 
     private fun KSAnnotated.isInKotlinCode(): Boolean {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 2bd5ed4..be3f4bf 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -159,6 +159,11 @@
                 class MyGenericIn<in T>
                 class MyGenericOut<out T>
                 class MyGenericMultipleParameters<T1: MyGeneric<*>, T2: MyGeneric<T1>>
+                interface MyInterface
+                typealias MyInterfaceAlias = MyInterface
+                typealias MyGenericAlias = MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>
+                typealias JSW = JvmSuppressWildcards
+                typealias JW = JvmWildcard
                 typealias MyLambdaTypeAlias = (@JvmWildcard MyType) -> @JvmWildcard MyType
                 enum class MyEnum {
                     VAL1,
@@ -341,6 +346,14 @@
                     sealedListChild: List<GrandParentSealed.Parent2.Child1>,
                     jvmWildcard: List<@JvmWildcard String>,
                     suppressJvmWildcard: List<@JvmSuppressWildcards Number>,
+                    suppressJvmWildcardsGeneric1: @JvmSuppressWildcards List<MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+                    suppressJvmWildcardsGeneric2: List<@JvmSuppressWildcards MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+                    suppressJvmWildcardsGeneric3: List<MyGenericOut<@JvmSuppressWildcards MyGenericIn<MyGeneric<MyType>>>>,
+                    suppressJvmWildcardsGeneric4: List<MyGenericOut<MyGenericIn<@JvmSuppressWildcards MyGeneric<MyType>>>>,
+                    interfaceAlias: List<MyInterfaceAlias>,
+                    genericAlias: List<MyGenericAlias>,
+                    jvmWildcardTypeAlias: List<@JW String>,
+                    suppressJvmWildcardTypeAlias: List<@JSW Number>,
                 ) {
                     var propWithFinalType: String = ""
                     var propWithOpenType: Number = 3
@@ -353,6 +366,16 @@
                     var propSealedListChild: List<GrandParentSealed.Parent2.Child1> = TODO()
                     @JvmSuppressWildcards
                     var propWithOpenTypeButSuppressAnnotation: Number = 3
+                    var genericVar: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    @JvmSuppressWildcards var suppressJvmWildcardsGenericVar1: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar2: @JvmSuppressWildcards List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar3: List<@JvmSuppressWildcards MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar4: List<MyGenericIn<@JvmSuppressWildcards MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar5: List<MyGenericIn<MyGenericOut<@JvmSuppressWildcards MyGenericOut<MyType>>>> = TODO()
+                    var interfaceAlias: List<MyInterfaceAlias> = TODO()
+                    var genericAlias: List<MyGenericAlias> = TODO()
+                    var jvmWildcardTypeAlias: List<@JW String> = TODO()
+                    var suppressJvmWildcardTypeAlias: List<@JSW Number> = TODO()
                     fun list(list: List<*>): List<*> { TODO() }
                     fun listTypeArg(list: List<R>): List<R> { TODO() }
                     fun listTypeArgNumber(list: List<Number>): List<Number> { TODO() }
@@ -393,6 +416,43 @@
                     fun suspendExplicitJvmSuppressWildcard_OnType2(
                         list: @JvmSuppressWildcards List<Number>
                     ): @JvmSuppressWildcards List<Number> { TODO() }
+                    fun interfaceAlias(
+                        param: List<MyInterfaceAlias>
+                    ): List<MyInterfaceAlias> = TODO()
+                    fun explicitJvmSuppressWildcardsOnAlias(
+                        param: List<@JvmSuppressWildcards MyInterfaceAlias>,
+                    ): List<@JvmSuppressWildcards MyInterfaceAlias> = TODO()
+                    fun genericAlias(param: List<MyGenericAlias>): List<MyGenericAlias> = TODO()
+                    fun explicitJvmSuppressWildcardsOnGenericAlias(
+                        param: List<@JvmSuppressWildcards MyGenericAlias>,
+                    ): List<@JvmSuppressWildcards MyGenericAlias> = TODO()
+                    fun explicitOutOnInvariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW MyGeneric<out MyGeneric<MyType>>
+                    ): @JSW MyGeneric<out MyGeneric<MyType>> { TODO() }
+                    fun explicitOutOnInvariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW MyGeneric<MyGeneric<out MyType>>
+                    ): @JSW MyGeneric<MyGeneric<out MyType>> { TODO() }
+                    fun explicitOutOnVariant_onType1(
+                        list: List<out List<Number>>
+                    ): List<out List<Number>> { TODO() }
+                    fun explicitOutOnVariant_onType2(
+                        list: List<List<out Number>>
+                    ): List<List<out Number>> { TODO() }
+                    fun explicitOutOnVariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW List<out List<Number>>
+                    ): @JSW List<out List<Number>> { TODO() }
+                    fun explicitOutOnVariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW List<List<out Number>>
+                    ): @JSW List<List<out Number>> { TODO() }
+                    fun explicitJvmWildcardTypeAlias(
+                        list: List<@JW String>
+                    ): List<@JW String> { TODO() }
+                    fun explicitJvmSuppressWildcardTypeAlias_OnType(
+                        list: List<@JSW Number>
+                    ): List<@JSW Number> { TODO() }
+                    fun explicitJvmSuppressWildcardTypeAlias_OnType2(
+                        list: @JSW List<Number>
+                    ): @JSW List<Number> { TODO() }
                 }
                 """.trimIndent()
             ), listOf(className)
diff --git a/testutils/testutils-fonts/build.gradle b/testutils/testutils-fonts/build.gradle
index dffd039..911cf9b 100644
--- a/testutils/testutils-fonts/build.gradle
+++ b/testutils/testutils-fonts/build.gradle
@@ -14,12 +14,20 @@
  * limitations under the License.
  */
 
+
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
-    id("kotlin-android")
+}
+
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 }
 
 dependencies {
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index f541247..1a5e97a 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.compose.navigation
 
+import android.util.Log
 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
@@ -171,9 +172,23 @@
     // no WearNavigator.Destinations were added to the navigation backstack (be sure to build
     // the NavGraph using androidx.wear.compose.navigation.composable) or because the last entry
     // was popped prior to navigating (instead, use navigate with popUpTo).
-    val current = if (backStack.isNotEmpty()) backStack.last() else throw IllegalArgumentException(
-        "The WearNavigator backstack is empty, there is no navigation destination to display."
-    )
+    // If the activity is using FLAG_ACTIVITY_NEW_TASK then it also needs to set
+    // FLAG_ACTIVITY_CLEAR_TASK, otherwise the activity will be created twice,
+    // the first of these with an empty backstack.
+    val current = backStack.lastOrNull()
+
+    if (current == null) {
+        val warningText =
+            "Current backstack entry is empty. Please ensure: \n" +
+                "1. The current WearNavigator navigation backstack is not empty (e.g. by using " +
+                "androidx.wear.compose.navigation.composable to build your nav graph). \n" +
+                "2. The last entry is not popped prior to navigation " +
+                "(instead, use navigate with popUpTo). \n" +
+                "3. If the activity uses FLAG_ACTIVITY_NEW_TASK you should also set " +
+                "FLAG_ACTIVITY_CLEAR_TASK to maintain the backstack consistency."
+
+        Log.w(TAG, warningText)
+    }
 
     val swipeState = state.swipeToDismissBoxState
     LaunchedEffect(swipeState.currentValue) {
@@ -200,7 +215,7 @@
         modifier = Modifier,
         hasBackground = previous != null,
         backgroundKey = previous?.id ?: SwipeToDismissKeys.Background,
-        contentKey = current.id,
+        contentKey = current?.id ?: SwipeToDismissKeys.Content,
         content = { isBackground ->
             BoxedStackEntryContent(if (isBackground) previous else current, stateHolder, modifier)
         }
@@ -279,3 +294,5 @@
         }
     }
 }
+
+private const val TAG = "SwipeDismissableNavHost"
\ No newline at end of file
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -56,6 +56,7 @@
   public class StateStore {
     method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+    field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
   }
 
   public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -56,6 +56,7 @@
   public class StateStore {
     method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+    field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
   }
 
   public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index 62aa8aa..6dd8857 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -58,6 +58,7 @@
     method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @UiThread public void setStateEntryValuesProto(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+    field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
   }
 
   public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
index 3ed31ac..bbae3dc 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
@@ -16,6 +16,11 @@
 
 package androidx.wear.protolayout.expression.pipeline;
 
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.UiThread;
+
 import java.util.List;
 
 /**
@@ -76,6 +81,15 @@
 
     @Override
     public void close() {
+        if (Looper.getMainLooper().isCurrentThread()) {
+            closeInternal();
+        } else {
+            new Handler(Looper.getMainLooper()).post(this::closeInternal);
+        }
+    }
+
+    @UiThread
+    private void closeInternal() {
         mNodes.stream()
                 .filter(n -> n instanceof DynamicDataSourceNode)
                 .forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 068ce4b..3deccea 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -250,8 +250,8 @@
 
         /**
          * Gets the quota manager used for limiting the total number of dynamic types in the
-         * pipeline, or {@code null} if there are no restriction on the number of dynamic types.
-         * If present, the quota manager is used to prevent unreasonably expensive expressions.
+         * pipeline, or {@code null} if there are no restriction on the number of dynamic types. If
+         * present, the quota manager is used to prevent unreasonably expensive expressions.
          */
         @Nullable
         public QuotaManager getDynamicTypesQuotaManager() {
@@ -303,8 +303,8 @@
         MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
         TimeGateway timeGateway = config.getTimeGateway();
         if (timeGateway == null) {
-                timeGateway = new TimeGatewayImpl(uiHandler);
-                ((TimeGatewayImpl) timeGateway).enableUpdates();
+            timeGateway = new TimeGatewayImpl(uiHandler);
+            ((TimeGatewayImpl) timeGateway).enableUpdates();
         }
         this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, timeGateway);
         if (config.getSensorGateway() != null) {
@@ -331,7 +331,7 @@
         if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCount())) {
             throw new EvaluationException(
                     "Dynamic type expression limit reached. Try making the dynamic type expression"
-                        + " shorter or reduce the number of dynamic type expressions.");
+                            + " shorter or reduce the number of dynamic type expressions.");
         }
         return boundDynamicType;
     }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
index c2ce371..f722bbb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
@@ -18,6 +18,8 @@
 
 import static java.util.stream.Collectors.toMap;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -41,13 +43,27 @@
  * must only be used from the UI thread.
  */
 public class StateStore {
+    /**
+     * Maximum number for state entries allowed for this {@link StateStore}.
+     *
+     * <p>The ProtoLayout state model is not designed to handle large volumes of layout provided
+     * state. So we limit the number of state entries to keep the on-the-wire size and state
+     * store update times manageable.
+     */
+    @SuppressLint("MinMaxConstant")
+    public static final int MAX_STATE_ENTRY_COUNT = 100;
     @NonNull private final Map<String, StateEntryValue> mCurrentState = new ArrayMap<>();
 
     @NonNull
     private final Map<String, Set<DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>>>
             mRegisteredCallbacks = new ArrayMap<>();
 
-    /** Creates a {@link StateStore}. */
+    /**
+     * Creates a {@link StateStore}.
+     *
+     * @throws IllegalStateException if number of initialState entries is greater than
+     * {@link StateStore#MAX_STATE_ENTRY_COUNT}.
+     */
     @NonNull
     public static StateStore create(
             @NonNull Map<String, StateEntryBuilders.StateEntryValue> initialState) {
@@ -56,6 +72,9 @@
 
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     public StateStore(@NonNull Map<String, StateEntryValue> initialState) {
+        if (initialState.size() > MAX_STATE_ENTRY_COUNT) {
+            throw stateTooLargeException(initialState.size());
+        }
         mCurrentState.putAll(initialState);
     }
 
@@ -63,6 +82,10 @@
      * Sets the given state, replacing the current state.
      *
      * <p>Informs registered listeners of changed values, invalidates removed values.
+     *
+     * @throws IllegalStateException if number of state entries is greater than
+     * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+     * will stay in place.
      */
     @UiThread
     public void setStateEntryValues(
@@ -74,10 +97,18 @@
      * Sets the given state, replacing the current state.
      *
      * <p>Informs registered listeners of changed values, invalidates removed values.
+     *
+     * @throws IllegalStateException if number of state entries is larger than
+     * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+     * will stay in place.
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     @UiThread
     public void setStateEntryValuesProto(@NonNull Map<String, StateEntryValue> newState) {
+        if (newState.size() > MAX_STATE_ENTRY_COUNT) {
+            throw stateTooLargeException(newState.size());
+        }
+
         // Figure out which nodes have actually changed.
         Set<String> removedKeys = getRemovedKeys(newState);
         Map<String, StateEntryValue> changedEntries = getChangedEntries(newState);
@@ -85,10 +116,9 @@
         Stream.concat(removedKeys.stream(), changedEntries.keySet().stream())
                 .forEach(
                         key -> {
-                            for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
-                                    callback :
-                                            mRegisteredCallbacks.getOrDefault(
-                                                    key, Collections.emptySet())) {
+                            for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> callback :
+                                    mRegisteredCallbacks.getOrDefault(
+                                            key, Collections.emptySet())) {
                                 callback.onPreUpdate();
                             }
                         });
@@ -168,4 +198,12 @@
         }
         return result;
     }
+
+    static IllegalStateException stateTooLargeException(int stateSize) {
+        return new IllegalStateException(
+                String.format(
+                        "Too many state entries: %d. The maximum number of allowed state entries "
+                                + "is %d.",
+                        stateSize, MAX_STATE_ENTRY_COUNT));
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
index f9f699d..18bae95 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
@@ -138,6 +138,7 @@
     }
 
     @Override
+    @UiThread
     public void close() {
         setUpdatesEnabled(false);
         registeredCallbacks.clear();
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
index d3a2bb4..ae1630d 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
@@ -16,6 +16,7 @@
 
 package androidx.wear.protolayout.expression.pipeline;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -38,6 +39,9 @@
 import org.mockito.InOrder;
 import org.mockito.Mockito;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @RunWith(AndroidJUnit4.class)
 public class StateStoreTest {
     @Rule public Expect mExpect = Expect.create();
@@ -48,6 +52,8 @@
                             "foo", buildStateEntry("bar"),
                             "baz", buildStateEntry("foobar")));
 
+    public StateStoreTest() {}
+
     @Test
     public void setBuilderApi() {
         mStateStoreUnderTest.setStateEntryValues(
@@ -58,6 +64,25 @@
     }
 
     @Test
+    public void initState_largeNumberOfEntries_throws() {
+        Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+        for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+            state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+        }
+        assertThrows(IllegalStateException.class, () -> StateStore.create(state));
+    }
+
+    @Test
+    public void newState_largeNumberOfEntries_throws() {
+        Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+        for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+            state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+        }
+        assertThrows(
+                IllegalStateException.class, () -> mStateStoreUnderTest.setStateEntryValues(state));
+    }
+
+    @Test
     public void canReadInitialState() {
         mExpect.that(mStateStoreUnderTest.getStateEntryValuesProto("foo"))
                 .isEqualTo(buildStateEntry("bar"));
@@ -88,8 +113,7 @@
 
     @Test
     public void setStateFiresListeners() {
-        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
-                buildStateUpdateCallbackMock();
+        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
         mStateStoreUnderTest.registerCallback("foo", cb);
 
         mStateStoreUnderTest.setStateEntryValuesProto(
@@ -101,8 +125,7 @@
 
     @Test
     public void setStateFiresOnPreStateUpdateFirst() {
-        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
-                buildStateUpdateCallbackMock();
+        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
 
         InOrder inOrder = Mockito.inOrder(cb);
 
@@ -166,8 +189,7 @@
     @SuppressWarnings("unchecked")
     @Test
     public void canUnregisterListeners() {
-        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
-                buildStateUpdateCallbackMock();
+        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
         mStateStoreUnderTest.registerCallback("foo", cb);
 
         mStateStoreUnderTest.setStateEntryValuesProto(
@@ -183,8 +205,7 @@
     }
 
     @SuppressWarnings("unchecked")
-    private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
-            buildStateUpdateCallbackMock() {
+    private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> buildStateUpdateCallbackMock() {
         // This needs an unchecked cast because of the generic; this method just centralizes the
         // warning suppression.
         return mock(DynamicTypeValueReceiverWithPreUpdate.class);
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
index 9b7d7ac..d89a564 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
@@ -168,7 +168,8 @@
     }
 
     @Test
-    public void buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+    public void
+            buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
         List<Float> results = new ArrayList<>();
         float endValue = 10.0f;
         DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -183,7 +184,7 @@
 
     @Test
     public void
-            buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+            buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
         List<Float> results = new ArrayList<>();
         float endValue = 10.0f;
         DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -217,7 +218,7 @@
 
     @Test
     public void
-            buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndVal() {
+            buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndValue() {
         List<Integer> results = new ArrayList<>();
         DynamicColor dynamicColor = animatableFixedColor(0, 1);
         ColorProp colorProp = ColorProp.newBuilder().setDynamicValue(dynamicColor).build();
@@ -1719,8 +1720,7 @@
                                                         Repeatable.newBuilder()
                                                                 .setRepeatMode(
                                                                         RepeatMode
-                                                                                .REPEAT_MODE_REVERSE
-                                                                )
+                                                                                .REPEAT_MODE_REVERSE)
                                                                 .setIterations(iterations)
                                                                 .setForwardRepeatOverride(
                                                                         alternateParameters)
@@ -1813,13 +1813,12 @@
         ProtoLayoutDynamicDataPipeline pipeline =
                 enableAnimations
                         ? new ProtoLayoutDynamicDataPipeline(
-                        /* sensorGateway= */ null,
+                                /* sensorGateway= */ null,
                                 mStateStore,
                                 new FixedQuotaManagerImpl(MAX_VALUE),
                                 new FixedQuotaManagerImpl(MAX_VALUE))
                         : new ProtoLayoutDynamicDataPipeline(
-                                /* sensorGateway= */ null,
-                                mStateStore);
+                                /* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1842,7 +1841,7 @@
         AddToListCallback<Float> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1860,7 +1859,7 @@
         AddToListCallback<Integer> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1878,7 +1877,7 @@
         AddToListCallback<Float> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1896,7 +1895,7 @@
         AddToListCallback<Float> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1914,7 +1913,7 @@
         AddToListCallback<Integer> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 9e263bf..9a529c2 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -4205,8 +4205,9 @@
     private static FadeInTransition.Builder fadeIn(int delay) {
         return FadeInTransition.newBuilder()
                 .setAnimationSpec(
-                        AnimationSpec.newBuilder().setAnimationParameters(
-                                AnimationParameters.newBuilder().setDelayMillis(delay)));
+                        AnimationSpec.newBuilder()
+                                .setAnimationParameters(
+                                        AnimationParameters.newBuilder().setDelayMillis(delay)));
     }
 
     private LayoutElement textFadeInSlideIn(String text) {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
index 8889ec0..0fb8101 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
@@ -149,9 +149,17 @@
         return this;
       }
 
+      private static final int MAX_STATE_SIZE = 30;
+
       /** Builds an instance from accumulated values. */
       @NonNull
       public State build() {
+        if (mImpl.getIdToValueMap().size() > MAX_STATE_SIZE) {
+          throw new IllegalStateException(
+                  String.format(
+                          "State size is too large: %d. Maximum " + "allowed state size is %d.",
+                          mImpl.getIdToValueMap().size(), MAX_STATE_SIZE));
+        }
         return new State(mImpl.build(), mFingerprint);
       }
     }
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 184aa81..3fb2267 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -202,15 +202,18 @@
     public View inflate(@NonNull ViewGroup parent) {
         String errorMessage =
                 "This method only works with the deprecated constructors that accept Layout and"
-                    + " Resources.";
+                        + " Resources.";
         try {
             // Waiting for the result from future for backwards compatibility.
             return inflateLayout(
-                    checkNotNull(mLayout, errorMessage),
-                    checkNotNull(mResources, errorMessage),
-                    parent).get(10, TimeUnit.SECONDS);
-        } catch (ExecutionException | InterruptedException | CancellationException |
-                 TimeoutException e) {
+                            checkNotNull(mLayout, errorMessage),
+                            checkNotNull(mResources, errorMessage),
+                            parent)
+                    .get(10, TimeUnit.SECONDS);
+        } catch (ExecutionException
+                | InterruptedException
+                | CancellationException
+                | TimeoutException e) {
             // Wrap checked exceptions to avoid changing the method signature.
             throw new RuntimeException("Rendering tile has not successfully finished.", e);
         }
@@ -219,13 +222,12 @@
     /**
      * Inflates a Tile into {@code parent}.
      *
-     * @param layout    The portion of the Tile to render.
+     * @param layout The portion of the Tile to render.
      * @param resources The resources for the Tile.
-     * @param parent    The view to attach the tile into.
+     * @param parent The view to attach the tile into.
      * @return The future with the first child that was inflated. This may be null if the Layout is
-     * empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
-     * contains an
-     * unsupported inner type.
+     *     empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
+     *     contains an unsupported inner type.
      */
     @NonNull
     public ListenableFuture<View> inflateAsync(
@@ -241,7 +243,6 @@
             @NonNull ResourceProto.Resources resources,
             @NonNull ViewGroup parent) {
         ListenableFuture<Void> result = mInstance.renderAndAttach(layout, resources, parent);
-            return FluentFuture.from(result)
-                    .transform(ignored -> parent.getChildAt(0), mUiExecutor);
+        return FluentFuture.from(result).transform(ignored -> parent.getChildAt(0), mUiExecutor);
     }
 }
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 8887171d..949c589 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -1836,13 +1836,13 @@
          *
          * Returns this Builder to allow chaining.
          */
-        fun setListEntryCollection(timelineEntries: Collection<ComplicationData>?) = apply {
-            if (timelineEntries == null) {
+        fun setListEntryCollection(listEntries: Collection<ComplicationData>?) = apply {
+            if (listEntries == null) {
                 fields.remove(EXP_FIELD_LIST_ENTRIES)
             } else {
                 fields.putParcelableArray(
                     EXP_FIELD_LIST_ENTRIES,
-                    timelineEntries
+                    listEntries
                         .map { data ->
                             data.fields.putInt(EXP_FIELD_LIST_ENTRY_TYPE, data.type)
                             data.fields
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index 94db677..ddf43de 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -18,7 +18,7 @@
 
 import android.icu.util.ULocale
 import android.support.wearable.complications.ComplicationData as WireComplicationData
-import android.support.wearable.complications.ComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
 import android.support.wearable.complications.ComplicationText as WireComplicationText
 import androidx.annotation.MainThread
 import androidx.annotation.RestrictTo
@@ -41,17 +41,19 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.updateAndGet
 import kotlinx.coroutines.invoke
 import kotlinx.coroutines.launch
 
 /**
  * Evaluates a [WireComplicationData] with
  * [androidx.wear.protolayout.expression.DynamicBuilders.DynamicType] within its fields.
- *
- * Due to [WireComplicationData]'s shallow copy strategy the input is modified in-place.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class ComplicationDataExpressionEvaluator(
@@ -65,27 +67,101 @@
      *
      * The expression is evaluated _separately_ on each flow collection.
      */
-    fun evaluate(unevaluatedData: WireComplicationData) =
-        flow<WireComplicationData> {
-            val state: MutableStateFlow<State> = unevaluatedData.buildState()
-            state.value.use {
-                val evaluatedData: Flow<WireComplicationData> =
-                    state
-                        .mapNotNull {
-                            when {
-                                // Emitting INVALID_DATA if there's an invalid receiver.
-                                it.invalidReceivers.isNotEmpty() -> INVALID_DATA
-                                // Emitting the data if all pending receivers are done and all
-                                // pre-updates are satisfied.
-                                it.pendingReceivers.isEmpty() -> it.data
-                                // Skipping states that are not ready for be emitted.
-                                else -> null
-                            }
-                        }
-                        .distinctUntilChanged()
-                emitAll(evaluatedData)
+    fun evaluate(unevaluatedData: WireComplicationData): Flow<WireComplicationData> =
+        evaluateTopLevelFields(unevaluatedData)
+            // Combining with fields that are made of WireComplicationData.
+            .combineWithDataList(unevaluatedData.timelineEntries) { entries ->
+                // Timeline entries are set on the built WireComplicationData.
+                WireComplicationData.Builder(
+                    this@combineWithDataList.build().apply { setTimelineEntryCollection(entries) }
+                )
             }
+            .combineWithDataList(unevaluatedData.listEntries) { setListEntryCollection(it) }
+            // Must be last, as it overwrites INVALID_DATA.
+            .combineWithEvaluatedPlaceholder(unevaluatedData.placeholder)
+            .distinctUntilChanged()
+
+    /** Evaluates "local" fields, excluding fields of type WireComplicationData. */
+    private fun evaluateTopLevelFields(
+        unevaluatedData: WireComplicationData
+    ): Flow<WireComplicationData> = flow {
+        val state: MutableStateFlow<State> = unevaluatedData.buildState()
+        state.value.use {
+            val evaluatedData: Flow<WireComplicationData> =
+                state.mapNotNull {
+                    when {
+                        // Emitting INVALID_DATA if there's an invalid receiver.
+                        it.invalidReceivers.isNotEmpty() -> INVALID_DATA
+                        // Emitting the data if all pending receivers are done and all
+                        // pre-updates are satisfied.
+                        it.pendingReceivers.isEmpty() -> it.data
+                        // Skipping states that are not ready for be emitted.
+                        else -> null
+                    }
+                }
+            emitAll(evaluatedData)
         }
+    }
+
+    /**
+     * Combines the receiver with the evaluated version of the provided list.
+     *
+     * If the receiver [Flow] emits [INVALID_DATA] or the input list is null or empty, this does not
+     * mutate the flow and does not wait for the entries to finish evaluating.
+     *
+     * If even one [WireComplicationData] within the provided list is evaluated to [INVALID_DATA],
+     * the output [Flow] becomes [INVALID_DATA] (the receiver [Flow] is ignored).
+     */
+    private fun Flow<WireComplicationData>.combineWithDataList(
+        unevaluatedEntries: List<WireComplicationData>?,
+        setter:
+            WireComplicationData.Builder.(
+                List<WireComplicationData>
+            ) -> WireComplicationData.Builder,
+    ): Flow<WireComplicationData> {
+        if (unevaluatedEntries.isNullOrEmpty()) return this
+        val evaluatedEntriesFlow: Flow<List<WireComplicationData>> =
+            combine(unevaluatedEntries.map { evaluate(it) })
+
+        return this.combine(evaluatedEntriesFlow).map {
+            (data: WireComplicationData, evaluatedEntries: List<WireComplicationData>?) ->
+            // Not mutating if invalid.
+            if (data === INVALID_DATA) return@map data
+            // An entry is invalid, emitting invalid.
+            if (evaluatedEntries.any { it === INVALID_DATA }) return@map INVALID_DATA
+            // All is well, mutating the input.
+            return@map WireComplicationData.Builder(data).setter(evaluatedEntries).build()
+        }
+    }
+
+    /**
+     * Same as [combineWithDataList], but sets the evaluated placeholder ONLY when the receiver
+     * [Flow] emits [TYPE_NO_DATA], or [keepExpression] is true, otherwise clears it and does not
+     * wait for the placeholder to finish evaluating.
+     *
+     * If the placeholder is not required (per the above paragraph), this doesn't wait for it.
+     */
+    private fun Flow<WireComplicationData>.combineWithEvaluatedPlaceholder(
+        unevaluatedPlaceholder: WireComplicationData?
+    ): Flow<WireComplicationData> {
+        if (unevaluatedPlaceholder == null) return this
+        val evaluatedPlaceholderFlow: Flow<WireComplicationData> = evaluate(unevaluatedPlaceholder)
+
+        return this.combine(evaluatedPlaceholderFlow).map {
+            (data: WireComplicationData, evaluatedPlaceholder: WireComplicationData?) ->
+            if (!keepExpression && data.type != TYPE_NO_DATA) {
+                // Clearing the placeholder when data is not TYPE_NO_DATA (it was meant as an
+                // expression fallback).
+                return@map WireComplicationData.Builder(data).setPlaceholder(null).build()
+            }
+            // Placeholder required but invalid, emitting invalid.
+            if (evaluatedPlaceholder === INVALID_DATA) return@map INVALID_DATA
+            // All is well, mutating the input.
+            return@map WireComplicationData.Builder(data)
+                .setPlaceholder(evaluatedPlaceholder)
+                .build()
+        }
+    }
 
     private suspend fun WireComplicationData.buildState() =
         MutableStateFlow(State(this)).apply {
@@ -177,7 +253,7 @@
      * [ComplicationEvaluationResultReceiver] that are evaluating it.
      */
     private inner class State(
-        val data: ComplicationData,
+        val data: WireComplicationData,
         val pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
         val invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
         val completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
@@ -317,3 +393,35 @@
         runnable.run()
     }
 }
+
+/** Replacement of [kotlinx.coroutines.flow.combine], which doesn't seem to work. */
+internal fun <T> combine(flows: List<Flow<T>>): Flow<List<T>> = flow {
+    data class ValueExists(val value: T?, val exists: Boolean)
+    val latest = MutableStateFlow(List(flows.size) { ValueExists(null, false) })
+    @Suppress("UNCHECKED_CAST") // Flow<List<T?>> -> Flow<List<T>> safe after filtering exists.
+    emitAll(
+        flows
+            .mapIndexed { i, flow -> flow.map { i to it } } // List<Flow<Int, T>> (indexed flows)
+            .merge() // Flow<Int, T>
+            .map { (i, value) ->
+                // Updating latest and returning the current latest.
+                latest.updateAndGet {
+                    val newLatest = it.toMutableList()
+                    newLatest[i] = ValueExists(value, true)
+                    newLatest
+                }
+            } // Flow<List<ValueExists>>
+            // Filtering emissions until we have all values.
+            .filter { values -> values.all { it.exists } }
+            // Flow<List<T>> + defensive copy.
+            .map { values -> values.map { it.value } } as Flow<List<T>>
+    )
+}
+
+/**
+ * Another replacement of [kotlinx.coroutines.flow.combine] which is similar to
+ * `combine(List<Flow<T>>)` but allows different types for each flow.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun <T1, T2> Flow<T1>.combine(other: Flow<T2>): Flow<Pair<T1, T2>> =
+    combine(listOf(this as Flow<*>, other as Flow<*>)).map { (a, b) -> (a as T1) to (b as T2) }
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index c913c25..3a22917 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -17,6 +17,8 @@
 package androidx.wear.watchface.complications.data
 
 import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_SHORT_TEXT
 import android.support.wearable.complications.ComplicationText as WireComplicationText
 import android.util.Log
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
@@ -58,10 +60,7 @@
 
     @Test
     fun evaluate_noExpression_returnsUnevaluated() = runBlocking {
-        val data =
-            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
-                .setRangedValue(10f)
-                .build()
+        val data = WireComplicationData.Builder(TYPE_NO_DATA).setRangedValue(10f).build()
 
         val evaluator = ComplicationDataExpressionEvaluator()
 
@@ -81,7 +80,7 @@
     ) {
         SET_IMMEDIATELY_WHEN_ALL_DATA_AVAILABLE(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(TYPE_NO_DATA)
                     .setRangedValueExpression(DynamicFloat.constant(1f))
                     .setLongText(WireComplicationText(DynamicString.constant("Long Text")))
                     .setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
@@ -90,23 +89,29 @@
                     .setContentDescription(
                         WireComplicationText(DynamicString.constant("Description"))
                     )
-                    .build(),
+                    .setPlaceholder(constantData("Placeholder"))
+                    .setListEntryCollection(listOf(constantData("List")))
+                    .build()
+                    .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) },
             states = listOf(),
             evaluated =
                 listOf(
-                    WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                    WireComplicationData.Builder(TYPE_NO_DATA)
                         .setRangedValue(1f)
                         .setLongText(WireComplicationText("Long Text"))
                         .setLongTitle(WireComplicationText("Long Title"))
                         .setShortText(WireComplicationText("Short Text"))
                         .setShortTitle(WireComplicationText("Short Title"))
                         .setContentDescription(WireComplicationText("Description"))
+                        .setPlaceholder(evaluatedData("Placeholder"))
+                        .setListEntryCollection(listOf(evaluatedData("List")))
                         .build()
+                        .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
                 ),
         ),
         SET_ONLY_AFTER_ALL_FIELDS_EVALUATED(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(TYPE_NO_DATA)
                     .setRangedValueExpression(DynamicFloat.fromState("ranged_value"))
                     .setLongText(WireComplicationText(DynamicString.fromState("long_text")))
                     .setLongTitle(WireComplicationText(DynamicString.fromState("long_title")))
@@ -115,7 +120,10 @@
                     .setContentDescription(
                         WireComplicationText(DynamicString.fromState("description"))
                     )
-                    .build(),
+                    .setPlaceholder(stateData("placeholder"))
+                    .setListEntryCollection(listOf(stateData("list")))
+                    .build()
+                    .also { it.setTimelineEntryCollection(listOf(stateData("timeline"))) },
             states =
                 aggregate(
                     // Each map piles on top of the previous ones.
@@ -124,25 +132,38 @@
                     mapOf("long_title" to StateEntryValue.fromString("Long Title")),
                     mapOf("short_text" to StateEntryValue.fromString("Short Text")),
                     mapOf("short_title" to StateEntryValue.fromString("Short Title")),
-                    // Only the last one will trigger an evaluated data.
                     mapOf("description" to StateEntryValue.fromString("Description")),
+                    mapOf("placeholder" to StateEntryValue.fromString("Placeholder")),
+                    mapOf("list" to StateEntryValue.fromString("List")),
+                    mapOf("timeline" to StateEntryValue.fromString("Timeline")),
+                    // Only the last one will trigger an evaluated data.
                 ),
             evaluated =
                 listOf(
-                    INVALID_DATA, // Before state is available.
-                    WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                    // Before any state is available.
+                    INVALID_DATA,
+                    // INVALID_DATA with placeholder, after it's available (and others aren't).
+                    WireComplicationData.Builder(INVALID_DATA)
+                        .setPlaceholder(evaluatedData("Placeholder"))
+                        .build(),
+                    // Evaluated data with after everything is available.
+                    WireComplicationData.Builder(TYPE_NO_DATA)
                         .setRangedValue(1f)
                         .setLongText(WireComplicationText("Long Text"))
                         .setLongTitle(WireComplicationText("Long Title"))
                         .setShortText(WireComplicationText("Short Text"))
                         .setShortTitle(WireComplicationText("Short Title"))
                         .setContentDescription(WireComplicationText("Description"))
+                        // Not trimmed for TYPE_NO_DATA.
+                        .setPlaceholder(evaluatedData("Placeholder"))
+                        .setListEntryCollection(listOf(evaluatedData("List")))
                         .build()
+                        .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
                 ),
         ),
         SET_TO_EVALUATED_IF_ALL_FIELDS_VALID(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
                     .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
                     .setShortText(WireComplicationText(DynamicString.fromState("valid")))
                     .build(),
@@ -153,7 +174,7 @@
             evaluated =
                 listOf(
                     INVALID_DATA, // Before state is available.
-                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                    WireComplicationData.Builder(TYPE_SHORT_TEXT)
                         .setShortTitle(WireComplicationText("Valid"))
                         .setShortText(WireComplicationText("Valid"))
                         .build(),
@@ -161,7 +182,7 @@
         ),
         SET_TO_NO_DATA_IF_FIRST_STATE_IS_INVALID(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
                     .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
                     .setShortText(WireComplicationText(DynamicString.fromState("invalid")))
                     .build(),
@@ -177,7 +198,7 @@
         ),
         SET_TO_NO_DATA_IF_LAST_STATE_IS_INVALID(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
                     .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
                     .setShortText(WireComplicationText(DynamicString.fromState("invalid")))
                     .build(),
@@ -192,13 +213,43 @@
             evaluated =
                 listOf(
                     INVALID_DATA, // Before state is available.
-                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                    WireComplicationData.Builder(TYPE_SHORT_TEXT)
                         .setShortTitle(WireComplicationText("Valid"))
                         .setShortText(WireComplicationText("Valid"))
                         .build(),
                     INVALID_DATA, // After it was invalidated.
                 ),
         ),
+        SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_IF_NOT_NO_DATA(
+            expressed =
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText("Text"))
+                    .setPlaceholder(evaluatedData("Placeholder"))
+                    .build(),
+            states = listOf(),
+            evaluated =
+                listOf(
+                    // No placeholder.
+                    WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText("Text"))
+                        .build(),
+                )
+        ),
+        SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_EVEN_IF_PLACEHOLDER_INVALID_IF_NOT_NO_DATA(
+            expressed =
+            WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText("Text"))
+                .setPlaceholder(stateData("placeholder"))
+                .build(),
+            states = listOf(), // placeholder state not set.
+            evaluated =
+            listOf(
+                // No placeholder.
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText("Text"))
+                    .build(),
+            )
+        ),
     }
 
     @Test
@@ -231,7 +282,7 @@
     @Test
     fun evaluate_cancelled_cleansUp() = runBlocking {
         val expressed =
-            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(TYPE_NO_DATA)
                 .setRangedValueExpression(
                     // Uses TimeGateway, which needs cleaning up.
                     DynamicInstant.withSecondsPrecision(Instant.EPOCH)
@@ -262,19 +313,22 @@
     @Test
     fun evaluate_keepExpression_doesNotTrimUnevaluatedExpression() = runBlocking {
         val expressed =
-            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(TYPE_NO_DATA)
                 .setRangedValueExpression(DynamicFloat.constant(1f))
                 .setLongText(WireComplicationText(DynamicString.constant("Long Text")))
                 .setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
                 .setShortText(WireComplicationText(DynamicString.constant("Short Text")))
                 .setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
                 .setContentDescription(WireComplicationText(DynamicString.constant("Description")))
+                .setPlaceholder(constantData("Placeholder"))
+                .setListEntryCollection(listOf(constantData("List")))
                 .build()
+                .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) }
         val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
 
         assertThat(evaluator.evaluate(expressed).firstOrNull())
             .isEqualTo(
-                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(TYPE_NO_DATA)
                     .setRangedValue(1f)
                     .setRangedValueExpression(DynamicFloat.constant(1f))
                     .setLongText(
@@ -292,6 +346,29 @@
                     .setContentDescription(
                         WireComplicationText("Description", DynamicString.constant("Description"))
                     )
+                    .setPlaceholder(evaluatedWithConstantData("Placeholder"))
+                    .setListEntryCollection(listOf(evaluatedWithConstantData("List")))
+                    .build()
+                    .also {
+                        it.setTimelineEntryCollection(listOf(evaluatedWithConstantData("Timeline")))
+                    },
+            )
+    }
+
+    @Test
+    fun evaluate_keepExpressionNotNoData_doesNotTrimPlaceholder() = runBlocking {
+        val expressed =
+            WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText("Text"))
+                .setPlaceholder(evaluatedData("Placeholder"))
+                .build()
+        val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
+
+        assertThat(evaluator.evaluate(expressed).firstOrNull())
+            .isEqualTo(
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText("Text"))
+                    .setPlaceholder(evaluatedData("Placeholder"))
                     .build()
             )
     }
@@ -300,5 +377,25 @@
         /** Converts `[{a: A}, {b: B}, {c: C}]` to `[{a: A}, {a: A, b: B}, {a: A, b: B, c: C}]`. */
         fun <K, V> aggregate(vararg maps: Map<K, V>): List<Map<K, V>> =
             maps.fold(listOf()) { acc, map -> acc + ((acc.lastOrNull() ?: mapOf()) + map) }
+
+        fun constantData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(DynamicString.constant(value)))
+                .build()
+
+        fun stateData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(DynamicString.fromState(value)))
+                .build()
+
+        fun evaluatedData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(value))
+                .build()
+
+        fun evaluatedWithConstantData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(value, DynamicString.constant(value)))
+                .build()
     }
 }