Merge "Handle children UseCase state changes." into androidx-main
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
index 8ca7900..bc68ffa 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
@@ -87,6 +87,36 @@
                       "key": "notAnnotation",
                       "value": "androidx.test.filters.FlakyTest"
                     }
+                  ],
+                  "additionalApkKeys": []
+                }
+            """.trimIndent()
+            )
+        )
+    }
+
+    @Test
+    fun testJsonAgainstGoldenAdditionalApkKey() {
+        builder.additionalApkKeys(listOf("customKey"))
+        MatcherAssert.assertThat(
+            builder.buildJson(),
+            CoreMatchers.`is`("""
+                {
+                  "name": "placeHolderAndroidTest.xml",
+                  "minSdkVersion": "15",
+                  "testSuiteTags": [
+                    "placeholder_tag"
+                  ],
+                  "testApk": "placeholder.apk",
+                  "testApkSha256": "123456",
+                  "instrumentationArgs": [
+                    {
+                      "key": "notAnnotation",
+                      "value": "androidx.test.filters.FlakyTest"
+                    }
+                  ],
+                  "additionalApkKeys": [
+                    "customKey"
                   ]
                 }
             """.trimIndent()
@@ -118,7 +148,8 @@
                       "key": "androidx.benchmark.dryRunMode.enable",
                       "value": "true"
                     }
-                  ]
+                  ],
+                  "additionalApkKeys": []
                 }
             """.trimIndent()
             )
@@ -147,7 +178,8 @@
                       "key": "notAnnotation",
                       "value": "androidx.test.filters.FlakyTest"
                     }
-                  ]
+                  ],
+                  "additionalApkKeys": []
                 }
             """.trimIndent()
             )
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index d6ec9a5..b5e2ada 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -342,6 +342,8 @@
 
     var disableDeviceTests = false
 
+    val additionalDeviceTestApkKeys = mutableListOf<String>()
+
     fun shouldEnforceKotlinStrictApiMode(): Boolean {
         return !legacyDisableKotlinStrictApiMode &&
             shouldConfigureApiTasks()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
index 1907576..52cc075 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
@@ -28,10 +28,11 @@
     lateinit var minSdk: String
     var runAllTests: Boolean = true
     var cleanupApks: Boolean = true
-    val tags: MutableList<String> = mutableListOf()
+    val tags = mutableListOf<String>()
     lateinit var testApkName: String
     lateinit var testApkSha256: String
     lateinit var testRunner: String
+    val additionalApkKeys = mutableListOf<String>()
 
     fun configName(configName: String) = apply { this.configName = configName }
     fun appApkName(appApkName: String) = apply { this.appApkName = appApkName }
@@ -43,6 +44,7 @@
     fun runAllTests(runAllTests: Boolean) = apply { this.runAllTests = runAllTests }
     fun cleanupApks(cleanupApks: Boolean) = apply { this.cleanupApks = cleanupApks }
     fun tag(tag: String) = apply { this.tags.add(tag) }
+    fun additionalApkKeys(keys: List<String>) = apply { additionalApkKeys.addAll(keys) }
     fun testApkName(testApkName: String) = apply { this.testApkName = testApkName }
     fun testApkSha256(testApkSha256: String) = apply { this.testApkSha256 = testApkSha256 }
     fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
@@ -72,7 +74,8 @@
             "testApkSha256" to testApkSha256,
             "appApk" to appApkName,
             "appApkSha256" to appApkSha256,
-            "instrumentationArgs" to instrumentationArgs
+            "instrumentationArgs" to instrumentationArgs,
+            "additionalApkKeys" to additionalApkKeys
         )
         if (isBenchmark && !isPostsubmit) {
             values["instrumentationArgs"]
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index ad5f8e4..ee7c132 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -33,6 +33,7 @@
 import org.gradle.api.tasks.PathSensitivity
 import org.gradle.api.tasks.TaskAction
 import java.io.File
+import org.gradle.api.provider.ListProperty
 
 /**
  * Writes a configuration file in
@@ -84,6 +85,9 @@
     @get:Input
     abstract val presubmit: Property<Boolean>
 
+    @get:Input
+    abstract val additionalApkKeys: ListProperty<String>
+
     @get:OutputFile
     abstract val outputXml: RegularFileProperty
 
@@ -150,6 +154,7 @@
                 .appApkSha256(sha256(File(appApkBuiltArtifact.outputFile)))
             testApkSha256Report.addFile(appName, appApkBuiltArtifact)
         }
+        configBuilder.additionalApkKeys(additionalApkKeys.get())
         val isPresubmit = presubmit.get()
         configBuilder.isPostsubmit(!isPresubmit)
         // Will be using the constrained configs for all devices api 26 and below.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 7a4f404..b890f55 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -81,6 +81,7 @@
 
         task.testFolder.set(artifacts.get(SingleArtifact.APK))
         task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+        task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
         task.outputXml.fileValue(File(getTestConfigDirectory(), xmlName))
         task.outputJson.fileValue(File(getTestConfigDirectory(), jsonName))
         task.shaReportOutput.fileValue(File(getTestConfigDirectory(), sha256XmlName))
@@ -337,9 +338,11 @@
     val configTask = getOrCreateMacrobenchmarkConfigTask(variantName)
     if (path.endsWith("macrobenchmark")) {
         configTask.configure { task ->
+            val androidXExtension = extensions.getByType<AndroidXExtension>()
             val fileNamePrefix = "${this.path.asFilenamePrefix()}$variantName"
             task.testFolder.set(artifacts.get(SingleArtifact.APK))
             task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+            task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
             task.outputXml.fileValue(
                 File(getTestConfigDirectory(), "$fileNamePrefix.xml")
             )
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index a38510d..3f4139a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -25,6 +25,8 @@
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
 import static androidx.core.util.Preconditions.checkArgument;
 
+import static java.util.UUID.randomUUID;
+
 import android.graphics.Rect;
 import android.util.Size;
 
@@ -54,6 +56,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 /**
  * A {@link Node} implementation that wraps around the public {@link SurfaceProcessor} interface.
@@ -328,6 +331,15 @@
     public abstract static class OutConfig {
 
         /**
+         * Unique ID of the config.
+         *
+         * <p> This is for making sure two {@link OutConfig} with the same value can be stored as
+         * different keys in a {@link HashMap}.
+         */
+        @NonNull
+        abstract UUID getUuid();
+
+        /**
          * The target {@link UseCase} of the output stream.
          */
         @CameraEffect.Targets
@@ -371,7 +383,8 @@
         @NonNull
         public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size,
                 boolean mirroring) {
-            return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size, mirroring);
+            return new AutoValue_SurfaceProcessorNode_OutConfig(randomUUID(), targets, cropRect,
+                    size, mirroring);
         }
     }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 5628d5f..c949451 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.core.processing
 
+import android.graphics.Matrix
 import android.graphics.Rect
 import android.graphics.SurfaceTexture
 import android.os.Build
@@ -109,6 +110,30 @@
     }
 
     @Test
+    fun identicalOutConfigs_returnDifferentEdges() {
+        // Arrange: create 2 OutConfig with identical values
+        createSurfaceProcessorNode()
+        val inputEdge = SurfaceEdge(
+            PREVIEW,
+            StreamSpec.builder(INPUT_SIZE).build(),
+            Matrix.IDENTITY_MATRIX,
+            true,
+            PREVIEW_CROP_RECT,
+            0,
+            false
+        )
+        val outConfig1 = OutConfig.of(inputEdge)
+        val outConfig2 = OutConfig.of(inputEdge)
+        val input = SurfaceProcessorNode.In.of(inputEdge, listOf(outConfig1, outConfig2))
+        // Act.
+        val output = node.transform(input)
+        // Assert: there are two outputs
+        assertThat(output).hasSize(2)
+        // Cleanup
+        inputEdge.close()
+    }
+
+    @Test
     fun transformInput_receivesSurfaceRequest() {
         // Arrange.
         createSurfaceProcessorNode()
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 88f7362..3c6a103 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -36,7 +36,7 @@
          */
         api("androidx.annotation:annotation:1.1.0")
         api("androidx.compose.animation:animation:1.2.1")
-        api("androidx.compose.runtime:runtime:1.3.1")
+        api(project(":compose:runtime:runtime"))
         api(project(":compose:ui:ui"))
 
         implementation(libs.kotlinStdlibCommon)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index 6db15c3..dbaeb55 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -29,15 +29,19 @@
 import androidx.compose.foundation.relocation.bringIntoViewResponder
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReusableContent
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.focus.onFocusEvent
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.Size
@@ -426,7 +430,11 @@
                 state = state
             ) {
                 items(items.size) {
-                    Box(Modifier.requiredSize(10.dp).testTag("$it").focusable())
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .testTag("$it")
+                            .focusable())
                 }
             }
         }
@@ -489,4 +497,42 @@
             assertThat(container2Pinned).isTrue()
         }
     }
+
+    @Test
+    fun reusingFocusedItem_itemIsNotFocusedAnymore() {
+        // Arrange.
+        val focusRequester = FocusRequester()
+        lateinit var state: FocusState
+        var key by mutableStateOf(0)
+        rule.setContent {
+            ReusableContent(key) {
+                BasicText(
+                    "focusableText",
+                    modifier = Modifier
+                        .testTag(focusTag)
+                        .focusRequester(focusRequester)
+                        .onFocusEvent { state = it }
+                        .focusable()
+                )
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(state.isFocused).isTrue()
+        }
+        rule.onNodeWithTag(focusTag)
+            .assertIsFocused()
+
+        // Act.
+        rule.runOnIdle {
+            key = 1
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(state.isFocused).isFalse()
+        }
+        rule.onNodeWithTag(focusTag)
+            .assertIsNotFocused()
+    }
 }
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 5ec853d..3c04288 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -350,6 +350,17 @@
   public final class ExposedDropdownMenuKt {
   }
 
+  @kotlin.jvm.JvmInline public final value class FabPosition {
+    field public static final androidx.compose.material3.FabPosition.Companion Companion;
+  }
+
+  public static final class FabPosition.Companion {
+    method public int getCenter();
+    method public int getEnd();
+    property public final int Center;
+    property public final int End;
+  }
+
   public final class FloatingActionButtonDefaults {
     method public androidx.compose.material3.FloatingActionButtonElevation bottomAppBarFabElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
@@ -597,7 +608,14 @@
     method @androidx.compose.runtime.Composable public static void RadioButton(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit>? onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.RadioButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
+  public final class ScaffoldDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getContentWindowInsets();
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets contentWindowInsets;
+    field public static final androidx.compose.material3.ScaffoldDefaults INSTANCE;
+  }
+
   public final class ScaffoldKt {
+    method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
   }
 
   public final class SearchBarDefaults {
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 8eec45e..a9fd87e 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -495,7 +495,7 @@
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExposedDropdownMenuBox(boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ExposedDropdownMenuBoxScope,kotlin.Unit> content);
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api @kotlin.jvm.JvmInline public final value class FabPosition {
+  @kotlin.jvm.JvmInline public final value class FabPosition {
     field public static final androidx.compose.material3.FabPosition.Companion Companion;
   }
 
@@ -820,14 +820,14 @@
     property public boolean isVisible;
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api public final class ScaffoldDefaults {
+  public final class ScaffoldDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getContentWindowInsets();
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets contentWindowInsets;
     field public static final androidx.compose.material3.ScaffoldDefaults INSTANCE;
   }
 
   public final class ScaffoldKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 5ec853d..3c04288 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -350,6 +350,17 @@
   public final class ExposedDropdownMenuKt {
   }
 
+  @kotlin.jvm.JvmInline public final value class FabPosition {
+    field public static final androidx.compose.material3.FabPosition.Companion Companion;
+  }
+
+  public static final class FabPosition.Companion {
+    method public int getCenter();
+    method public int getEnd();
+    property public final int Center;
+    property public final int End;
+  }
+
   public final class FloatingActionButtonDefaults {
     method public androidx.compose.material3.FloatingActionButtonElevation bottomAppBarFabElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
@@ -597,7 +608,14 @@
     method @androidx.compose.runtime.Composable public static void RadioButton(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit>? onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.RadioButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
+  public final class ScaffoldDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getContentWindowInsets();
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets contentWindowInsets;
+    field public static final androidx.compose.material3.ScaffoldDefaults INSTANCE;
+  }
+
   public final class ScaffoldKt {
+    method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
   }
 
   public final class SearchBarDefaults {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
index bf5bc93..605e1fb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
@@ -58,7 +58,6 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalMaterial3Api::class)
 class ScaffoldTest {
 
     @get:Rule
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index 52d652c..122937e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -70,7 +70,6 @@
  * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
  * the child of the scroll, and not on the scroll itself.
  */
-@ExperimentalMaterial3Api
 @Composable
 fun Scaffold(
     modifier: Modifier = Modifier,
@@ -109,7 +108,6 @@
  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
  * [content], typically a [NavigationBar].
  */
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun ScaffoldLayout(
     fabPosition: FabPosition,
@@ -273,7 +271,6 @@
 /**
  * Object containing various default values for [Scaffold] component.
  */
-@ExperimentalMaterial3Api
 object ScaffoldDefaults {
     /**
      * Default insets to be used and consumed by the scaffold content slot
@@ -286,7 +283,6 @@
 /**
  * The possible positions for a [FloatingActionButton] attached to a [Scaffold].
  */
-@ExperimentalMaterial3Api
 @kotlin.jvm.JvmInline
 value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
     companion object {
diff --git a/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt b/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt
index d167cda..01283ce 100644
--- a/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt
+++ b/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt
@@ -158,6 +158,7 @@
     method public final boolean isAttached();
     method public void onAttach();
     method public void onDetach();
+    method public void onReset();
     method public final void sideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
     property public final boolean isAttached;
     property public final androidx.compose.ui.Modifier.Node node;
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index d167cda..01283ce 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -158,6 +158,7 @@
     method public final boolean isAttached();
     method public void onAttach();
     method public void onDetach();
+    method public void onReset();
     method public final void sideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
     property public final boolean isAttached;
     property public final androidx.compose.ui.Modifier.Node node;
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
index 56d110a..01ad27a 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
@@ -26,6 +26,9 @@
 import androidx.compose.material.Button
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -313,3 +316,20 @@
         }
     )
 }
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Sampled
+@Composable
+fun ModifierNodeResetSample() {
+    class SelectableNode : Modifier.Node() {
+        var selected by mutableStateOf(false)
+
+        override fun onReset() {
+            // reset `selected` to the initial value as if the node will be reused for
+            // displaying different content it shouldn't be selected straight away.
+            selected = false
+        }
+
+        // some logic which sets `selected` to true when it is selected
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 1e844e4..4358f39 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -2978,7 +2978,7 @@
         val child = rule.onNodeWithTag("child").fetchSemanticsNode()
 
         rule.runOnIdle {
-            child.layoutNode.innerCoordinator.detach()
+            child.layoutNode.innerCoordinator.onRelease()
         }
 
         rule.runOnIdle {
@@ -3006,8 +3006,8 @@
         val grandChild1 = rule.onNodeWithTag("grandChild1").fetchSemanticsNode()
         val grandChild2 = rule.onNodeWithTag("grandChild2").fetchSemanticsNode()
         rule.runOnIdle {
-            grandChild1.layoutNode.innerCoordinator.detach()
-            grandChild2.layoutNode.innerCoordinator.detach()
+            grandChild1.layoutNode.innerCoordinator.onRelease()
+            grandChild2.layoutNode.innerCoordinator.onRelease()
         }
 
         rule.runOnIdle {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
index 9e3fc69..0bab1a5 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
@@ -1305,7 +1305,10 @@
 
         assertThat(activeLayers).isEqualTo(1)
 
-        root.removeAll()
+        val node = root.children[0]
+        root.removeAt(0, 1)
+        // in the real composition after removing the node onRelease() will be called as well
+        node.onRelease()
         delegate.measureAndLayout()
 
         assertThat(activeLayers).isEqualTo(0)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
new file mode 100644
index 0000000..1352f5f
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
@@ -0,0 +1,654 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalComposeUiApi::class)
+
+package androidx.compose.ui.modifier
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReusableContent
+import androidx.compose.runtime.ReusableContentHost
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ObserverNode
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ModifierNodeReuseAndDeactivationTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun reusingCallsResetOnModifier() {
+        var reuseKey by mutableStateOf(0)
+
+        var resetCalls = 0
+
+        rule.setContent {
+            ReusableContent(reuseKey) {
+                TestLayout(onReset = { resetCalls++ })
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(resetCalls).isEqualTo(0)
+            reuseKey = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(resetCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun nodeIsNotRecreatedWhenReused() {
+        var reuseKey by mutableStateOf(0)
+
+        var createCalls = 0
+
+        rule.setContent {
+            ReusableContent(reuseKey) {
+                TestLayout(onCreate = { createCalls++ })
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(createCalls).isEqualTo(1)
+            reuseKey = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(createCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun resetIsCalledWhenContentIsDeactivated() {
+        var active by mutableStateOf(true)
+        var resetCalls = 0
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    TestLayout(onReset = { resetCalls++ })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(resetCalls).isEqualTo(0)
+            active = false
+        }
+
+        rule.runOnIdle {
+            assertThat(resetCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun resetIsCalledAgainWhenContentIsReactivated() {
+        var active by mutableStateOf(true)
+        var resetCalls = 0
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    TestLayout(onReset = { resetCalls++ })
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            active = false
+        }
+
+        rule.runOnIdle {
+            active = true
+        }
+
+        rule.runOnIdle {
+            assertThat(resetCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun updateIsNotCalledWhenReusedWithTheSameParams() {
+        var reuseKey by mutableStateOf(0)
+        var updateCalls = 0
+
+        rule.setContent {
+            ReusableContent(reuseKey) {
+                TestLayout(
+                    key = 1,
+                    onUpdate = { updateCalls++ }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(updateCalls).isEqualTo(0)
+            reuseKey++
+        }
+
+        rule.runOnIdle {
+            assertThat(updateCalls).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun updateIsCalledWhenReusedWithDifferentParam() {
+        var reuseKey by mutableStateOf(0)
+        var updateCalls = 0
+
+        rule.setContent {
+            ReusableContent(reuseKey) {
+                TestLayout(
+                    key = reuseKey,
+                    onUpdate = { updateCalls++ }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(updateCalls).isEqualTo(0)
+            reuseKey++
+        }
+
+        rule.runOnIdle {
+            assertThat(updateCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun nodesAreDetachedWhenReused() {
+        var reuseKey by mutableStateOf(0)
+
+        var onResetCalls = 0
+        var onAttachCalls = 0
+        var onResetCallsWhenDetached: Int? = null
+
+        rule.setContent {
+            ReusableContent(reuseKey) {
+                TestLayout(
+                    onAttach = { onAttachCalls++ },
+                    onReset = { onResetCalls++ },
+                    onDetach = { onResetCallsWhenDetached = onResetCalls }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(onAttachCalls).isEqualTo(1)
+            assertThat(onResetCallsWhenDetached).isNull()
+            reuseKey = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(onResetCalls).isEqualTo(1)
+            // makes sure onReset is called before detach:
+            assertThat(onResetCallsWhenDetached).isEqualTo(1)
+            assertThat(onAttachCalls).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun nodesAreDetachedAndAttachedWhenDeactivatedAndReactivated() {
+        var active by mutableStateOf(true)
+
+        var onResetCalls = 0
+        var onAttachCalls = 0
+        var onResetCallsWhenDetached: Int? = null
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    TestLayout(
+                        onAttach = { onAttachCalls++ },
+                        onReset = { onResetCalls++ },
+                        onDetach = { onResetCallsWhenDetached = onResetCalls }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(onAttachCalls).isEqualTo(1)
+            assertThat(onResetCallsWhenDetached).isNull()
+            active = false
+        }
+
+        rule.runOnIdle {
+            assertThat(onResetCalls).isEqualTo(1)
+            // makes sure onReset is called before detach:
+            assertThat(onResetCallsWhenDetached).isEqualTo(1)
+            assertThat(onAttachCalls).isEqualTo(1)
+            active = true
+        }
+
+        rule.runOnIdle {
+            assertThat(onAttachCalls).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun reusingStatelessModifierNotCausingInvalidation() {
+        var active by mutableStateOf(true)
+        var reuseKey by mutableStateOf(0)
+
+        var invalidations = 0
+        val onInvalidate: () -> Unit = {
+            invalidations++
+        }
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(reuseKey) {
+                    Layout(
+                        modifier = StatelessModifierElement(onInvalidate),
+                        measurePolicy = MeasurePolicy
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(1)
+            active = false
+        }
+
+        rule.runOnIdle {
+            active = true
+            reuseKey = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun reusingStatelessModifierWithUpdatedInputCausingInvalidation() {
+        var active by mutableStateOf(true)
+        var reuseKey by mutableStateOf(0)
+        var size by mutableStateOf(10)
+
+        var invalidations = 0
+        val onInvalidate: () -> Unit = {
+            invalidations++
+        }
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(reuseKey) {
+                    Layout(
+                        modifier = StatelessModifierElement(onInvalidate, size),
+                        measurePolicy = MeasurePolicy
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(1)
+            active = false
+        }
+
+        rule.runOnIdle {
+            active = true
+            reuseKey = 1
+            size = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun reusingModifierCausingInvalidationOnDelegatedInnerNode() {
+        var reuseKey by mutableStateOf(0)
+
+        var resetCalls = 0
+        val onReset: () -> Unit = {
+            resetCalls++
+        }
+
+        rule.setContent {
+            ReusableContent(reuseKey) {
+                Layout(
+                    modifier = DelegatingModifierElement(onReset),
+                    measurePolicy = MeasurePolicy
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(resetCalls).isEqualTo(0)
+            reuseKey = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(resetCalls).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun reusingModifierReadingStateInLayerBlock() {
+        var active by mutableStateOf(true)
+        var counter by mutableStateOf(0)
+
+        var invalidations = 0
+        val layerBlock: () -> Unit = {
+            // state read
+            counter.toString()
+            invalidations++
+        }
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    Layout(
+                        modifier = LayerModifierElement(layerBlock),
+                        measurePolicy = MeasurePolicy
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(1)
+            active = false
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(1)
+            counter++
+        }
+
+        rule.runOnIdle {
+            active = true
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(2)
+            counter++
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(3)
+        }
+    }
+
+    @Test
+    fun reusingModifierObservingState() {
+        var active by mutableStateOf(true)
+        var counter by mutableStateOf(0)
+
+        var invalidations = 0
+        val observedBlock: () -> Unit = {
+            // state read
+            counter.toString()
+            invalidations++
+        }
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    Layout(
+                        modifier = ObserverModifierElement(observedBlock),
+                        measurePolicy = MeasurePolicy
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(1)
+            active = false
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(1)
+            counter++
+        }
+
+        rule.runOnIdle {
+            active = true
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(2)
+            counter++
+        }
+
+        rule.runOnIdle {
+            assertThat(invalidations).isEqualTo(3)
+        }
+    }
+
+    @Test
+    fun reusingModifierLocalProviderAndConsumer() {
+        val key = modifierLocalOf { -1 }
+        var active by mutableStateOf(true)
+        var providedValue by mutableStateOf(0)
+
+        var receivedValue: Int? = null
+
+        rule.setContent {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    Layout(
+                        modifier = Modifier
+                            .modifierLocalProvider(key) { providedValue }
+                            .modifierLocalConsumer { receivedValue = key.current },
+                        measurePolicy = MeasurePolicy
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(receivedValue).isEqualTo(0)
+            active = false
+        }
+
+        rule.runOnIdle {
+            providedValue = 1
+        }
+
+        rule.runOnIdle {
+            active = true
+        }
+
+        rule.runOnIdle {
+            assertThat(receivedValue).isEqualTo(1)
+        }
+    }
+}
+
+@Composable
+private fun TestLayout(
+    key: Any? = null,
+    onReset: () -> Unit = {},
+    onCreate: () -> Unit = {},
+    onUpdate: () -> Unit = {},
+    onDetach: () -> Unit = {},
+    onAttach: () -> Unit = {}
+) {
+    val currentOnReset by rememberUpdatedState(onReset)
+    val currentOnCreate by rememberUpdatedState(onCreate)
+    val currentOnUpdate by rememberUpdatedState(onUpdate)
+    val currentOnDetach by rememberUpdatedState(onDetach)
+    val currentOnAttach by rememberUpdatedState(onAttach)
+    Layout(
+        modifier = createModifier(
+            key = key,
+            onCreate = { currentOnCreate.invoke() },
+            onUpdate = { currentOnUpdate.invoke() },
+            onReset = { currentOnReset.invoke() },
+            onDetach = { currentOnDetach.invoke() },
+            onAttach = { currentOnAttach.invoke() },
+        ),
+        measurePolicy = MeasurePolicy
+    )
+}
+
+private fun createModifier(
+    key: Any? = null,
+    onCreate: () -> Unit = {},
+    onUpdate: () -> Unit = {},
+    onReset: () -> Unit = {},
+    onDetach: () -> Unit = {},
+    onAttach: () -> Unit = {},
+): Modifier {
+    return modifierElementOf(
+        key = key,
+        create = {
+            onCreate()
+            object : Modifier.Node() {
+                override fun onReset() = onReset()
+                override fun onAttach() = onAttach()
+                override fun onDetach() = onDetach()
+                }
+        },
+        update = {
+            onUpdate()
+        }
+    ) {}
+}
+
+private val MeasurePolicy = MeasurePolicy { _, _ ->
+    layout(100, 100) { }
+}
+
+private class StatelessModifierElement(
+    private val onInvalidate: () -> Unit,
+    private val size: Int = 10
+) : ModifierNodeElement<StatelessModifierElement.Node>(params = size, inspectorInfo = {}) {
+    override fun create() = Node(size, onInvalidate)
+
+    override fun update(node: Node) = node.also {
+        it.size = size
+        it.onMeasure = onInvalidate
+    }
+
+    class Node(var size: Int, var onMeasure: () -> Unit) : Modifier.Node(), LayoutModifierNode {
+        override fun MeasureScope.measure(
+            measurable: Measurable,
+            constraints: Constraints
+        ): MeasureResult {
+            val placeable = measurable.measure(Constraints.fixed(size, size))
+            onMeasure()
+            return layout(placeable.width, placeable.height) {
+                placeable.place(0, 0)
+            }
+        }
+    }
+}
+
+private class DelegatingModifierElement(
+    private val onDelegatedNodeReset: () -> Unit,
+) : ModifierNodeElement<DelegatingModifierElement.Node>(inspectorInfo = {}) {
+    override fun create() = Node(onDelegatedNodeReset)
+
+    override fun update(node: Node) = node.also {
+        it.onReset = onDelegatedNodeReset
+    }
+
+    class Node(var onReset: () -> Unit) : DelegatingNode() {
+        private val inner = delegated {
+            object : Modifier.Node() {
+                override fun onReset() {
+                    this@Node.onReset.invoke()
+                }
+            }
+        }
+    }
+}
+
+private class LayerModifierElement(
+    private val layerBlock: () -> Unit,
+) : ModifierNodeElement<LayerModifierElement.Node>(inspectorInfo = {}) {
+    override fun create() = Node(layerBlock)
+
+    override fun update(node: Node) = node.also {
+        it.layerBlock = layerBlock
+    }
+
+    class Node(var layerBlock: () -> Unit) : Modifier.Node(), LayoutModifierNode {
+        override fun MeasureScope.measure(
+            measurable: Measurable,
+            constraints: Constraints
+        ): MeasureResult {
+            val placeable = measurable.measure(constraints)
+            return layout(placeable.width, placeable.height) {
+                placeable.placeWithLayer(0, 0) {
+                    layerBlock.invoke()
+                }
+            }
+        }
+    }
+}
+
+private class ObserverModifierElement(
+    private val observedBlock: () -> Unit,
+) : ModifierNodeElement<ObserverModifierElement.Node>(inspectorInfo = {}) {
+    override fun create() = Node(observedBlock)
+
+    override fun update(node: Node) = node.also {
+        it.observedBlock = observedBlock
+    }
+
+    class Node(var observedBlock: () -> Unit) : Modifier.Node(), ObserverNode {
+
+        override fun onAttach() {
+            observe()
+        }
+
+        private fun observe() {
+            observeReads {
+                observedBlock()
+            }
+        }
+
+        override fun onObservedReadsChanged() {
+            observe()
+        }
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 0d873b0..99859f7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -175,11 +175,13 @@
         internal var ownerScope: ModifierNodeOwnerScope? = null
         internal var coordinator: NodeCoordinator? = null
             private set
+        internal var insertedNodeAwaitingAttachForInvalidation = false
+        internal var updatedNodeAwaitingAttachForInvalidation = false
         /**
-         * Indicates that the node is attached and part of the tree. This will get set to true
-         * right before [onAttach] is called, and set to false right after [onDetach] is called.
-         *
-         * A Node will never be attached more than once.
+         * Indicates that the node is attached to a [androidx.compose.ui.layout.Layout] which is
+         * part of the UI tree.
+         * This will get set to true right before [onAttach] is called, and set to false right
+         * after [onDetach] is called.
          *
          * @see onAttach
          * @see onDetach
@@ -194,7 +196,7 @@
         @Suppress("NOTHING_TO_INLINE")
         internal inline fun isKind(kind: NodeKind<*>) = kindSet and kind.mask != 0
 
-        internal fun attach() {
+        internal open fun attach() {
             check(!isAttached)
             check(coordinator != null)
             isAttached = true
@@ -202,7 +204,7 @@
             // TODO(lmr): run side effects?
         }
 
-        internal fun detach() {
+        internal open fun detach() {
             check(isAttached)
             check(coordinator != null)
             onDetach()
@@ -211,13 +213,23 @@
             // TODO(lmr): cancel jobs / side effects?
         }
 
+        internal open fun reset() {
+            check(isAttached)
+            onReset()
+        }
+
         /**
+         * Called when the node is attached to a [androidx.compose.ui.layout.Layout] which is
+         * part of the UI tree.
          * When called, `node` is guaranteed to be non-null. You can call sideEffect,
          * coroutineScope, etc.
          */
         open fun onAttach() {}
 
         /**
+         * Called when the node is not attached to a [androidx.compose.ui.layout.Layout] which is
+         * not a part of the UI tree anymore. Note that the node can be reattached again.
+         *
          * This should be called right before the node gets removed from the list, so you should
          * still be able to traverse inside of this method. Ideally we would not allow you to
          * trigger side effects here.
@@ -225,6 +237,22 @@
         open fun onDetach() {}
 
         /**
+         * Called when the node is about to be moved to a pool of layouts ready to be reused.
+         * For example it happens when the node is part of the item of LazyColumn after this item
+         * is scrolled out of the viewport. This means this node could be in future reused for a
+         * [androidx.compose.ui.layout.Layout] displaying a semantically different content when
+         * the list will be populating a new item.
+         *
+         * Use this callback to reset some local item specific state, like "is my component focused".
+         *
+         * This callback is called while the node is attached. Right after this callback the node
+         * will be detached and later reattached when reused.
+         *
+         * @sample androidx.compose.ui.samples.ModifierNodeResetSample
+         */
+        open fun onReset() {}
+
+        /**
          * This can be called to register [effect] as a function to be executed after all of the
          * changes to the tree are applied.
          *
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
index 61519df..b73a8cb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
@@ -54,14 +54,21 @@
         if (previousFocusState != focusState) refreshFocusEventNodes()
     }
 
-    internal fun onRemoved() {
+    /**
+     * Clears focus if this focus target has it.
+     */
+    override fun onReset() {
         when (focusState) {
             // Clear focus from the current FocusTarget.
             // This currently clears focus from the entire hierarchy, but we can change the
             // implementation so that focus is sent to the immediate focus parent.
             Active, Captured -> requireOwner().focusOwner.clearFocus(force = true)
-
-            ActiveParent, Inactive -> scheduleInvalidationForFocusEvents()
+            ActiveParent -> {
+                scheduleInvalidationForFocusEvents()
+                // This node might be reused, so reset the state to Inactive.
+                focusStateImpl = Inactive
+            }
+            Inactive -> scheduleInvalidationForFocusEvents()
         }
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
index a449536..620349c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
@@ -63,7 +63,7 @@
         val toUpdate = hashSetOf<BackwardsCompatNode>()
         removed.forEachIndexed { i, layout ->
             val key = removedLocal[i]
-            if (layout.isAttached) {
+            if (layout.nodes.head.isAttached) {
                 // if the layout is still attached, that means that this provider got removed and
                 // there's possible some consumers below it that need to be updated
                 invalidateConsumersOfNodeForKey(layout.nodes.head, key, toUpdate)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index af4d5d2..65f3f8e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -153,7 +153,9 @@
             if (element is DrawCacheModifier) {
                 invalidateCache = true
             }
-            invalidateLayer()
+            if (!duringAttach) {
+                invalidateLayer()
+            }
         }
         if (isKind(Nodes.Layout)) {
             val isChainUpdate = requireLayoutNode().nodes.tail.isAttached
@@ -163,8 +165,10 @@
                 coordinator.layoutModifierNode = this
                 coordinator.onLayoutModifierNodeChanged()
             }
-            invalidateLayer()
-            requireLayoutNode().invalidateMeasurements()
+            if (!duringAttach) {
+                invalidateLayer()
+                requireLayoutNode().invalidateMeasurements()
+            }
         }
         if (element is RemeasurementModifier) {
             element.onRemeasurementAvailable(this)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
index 3b5ac23..8bbe3a8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
@@ -80,16 +80,21 @@
         }
     }
 
-    override fun onAttach() {
-        super.onAttach()
+    override fun attach() {
+        super.attach()
         forEachDelegate {
             it.updateCoordinator(coordinator)
             it.attach()
         }
     }
 
-    override fun onDetach() {
+    override fun detach() {
         forEachDelegate { it.detach() }
-        super.onDetach()
+        super.detach()
+    }
+
+    override fun reset() {
+        super.reset()
+        forEachDelegate { it.reset() }
     }
 }
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 a410193..2c425f0 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
@@ -15,6 +15,7 @@
  */
 package androidx.compose.ui.node
 
+import androidx.compose.runtime.ComposeNodeLifecycleCallback
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -73,7 +74,11 @@
     private val isVirtual: Boolean = false,
     // The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode.
     override val semanticsId: Int = generateSemanticsId()
-) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,
+) : ComposeNodeLifecycleCallback,
+    Remeasurement,
+    OwnerScope,
+    LayoutInfo,
+    ComposeUiNode,
     Owner.OnLayoutCompletedListener {
 
     val isPlacedInLookahead: Boolean?
@@ -400,7 +405,7 @@
         invalidateMeasurements()
         parent?.invalidateMeasurements()
 
-        forEachCoordinatorIncludingInner { it.attach() }
+        forEachCoordinatorIncludingInner { it.onLayoutNodeAttach() }
         onAttach?.invoke(owner)
 
         invalidateFocusOnAttach()
@@ -425,7 +430,6 @@
         }
         layoutDelegate.resetAlignmentLines()
         onDetach?.invoke(owner)
-        forEachCoordinatorIncludingInner { it.detach() }
 
         @OptIn(ExperimentalComposeUiApi::class)
         if (outerSemantics != null) {
@@ -748,7 +752,6 @@
      */
     override var modifier: Modifier = Modifier
         set(value) {
-            if (value == field) return
             require(!isVirtual || modifier === Modifier) {
                 "Modifiers are not supported on virtual LayoutNodes"
             }
@@ -764,6 +767,10 @@
             layoutDelegate.updateParentData()
         }
 
+    private fun resetModifierState() {
+        nodes.resetState()
+    }
+
     internal fun invalidateParentData() {
         layoutDelegate.invalidateParentData()
     }
@@ -1346,6 +1353,26 @@
     override val parentInfo: LayoutInfo?
         get() = parent
 
+    private var deactivated = false
+
+    override fun onReuse() {
+        if (deactivated) {
+            deactivated = false
+            // we don't need to reset state as it was done when deactivated
+        } else {
+            resetModifierState()
+        }
+    }
+
+    override fun onDeactivate() {
+        deactivated = true
+        resetModifierState()
+    }
+
+    override fun onRelease() {
+        forEachCoordinatorIncludingInner { it.onRelease() }
+    }
+
     internal companion object {
         private val ErrorMeasurePolicy: NoIntrinsicsMeasurePolicy =
             object : NoIntrinsicsMeasurePolicy(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
index 6116c55..a936861 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
@@ -56,7 +56,7 @@
 ) : Modifier.Element, InspectorValueInfo(inspectorInfo) {
     /**
      * This will be called the first time the modifier is applied to the Layout and it should
-     * construct and return the correspoding [Modifier.Node] instance.
+     * construct and return the corresponding [Modifier.Node] instance.
      */
     abstract fun create(): N
 
@@ -85,7 +85,7 @@
  *
  * @param key An object used to determine whether or not the created node should be updated or not.
  * @param create The initial creation of the node. This will be called the first time the modifier
- *  is applied to the Layout and it should construct the correspoding [Modifier.Node] instance,
+ *  is applied to the Layout and it should construct the corresponding [Modifier.Node] instance,
  *  referencing any captured inputs necessary.
  * @param update Called when a modifier is applied to a Layout whose [key] have changed from the
  *  previous application. This lambda will have the current node instance passed in as a parameter,
@@ -123,7 +123,7 @@
  * which accepts a "params" and "update" parameter.
  *
  * @param create The initial creation of the node. This will be called the first time the modifier
- *  is applied to the Layout and it should construct the correspoding [Modifier.Node] instance
+ *  is applied to the Layout and it should construct the corresponding [Modifier.Node] instance
  * @param definitions This lambda will construct a debug-only set of information for use with
  *  tooling.
  *
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 e1d54e7..e61d347 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
@@ -97,7 +97,7 @@
         // to avoid allocating vectors every time modifier is set, we have two vectors that we
         // reuse over time. Since the common case is the modifier chains will be of equal length,
         // these vectors should be sized appropriately
-        val before = current ?: mutableVectorOf()
+        val before = current ?: MutableVector(capacity = 0)
         val after = m.fillVector(buffer ?: mutableVectorOf())
         if (after.size == before.size) {
             // assume if the sizes are the same, that we are in a common case of no structural
@@ -111,7 +111,7 @@
             while (node != null && i >= 0) {
                 val prev = before[i]
                 val next = after[i]
-                when (reuseActionForModifiers(prev, next)) {
+                when (actionForModifiers(prev, next)) {
                     ActionReplace -> {
                         // TODO(lmr): we could avoid running the diff if i = 0, since that would
                         //  always be simple remove + insert
@@ -128,8 +128,6 @@
                         // reuse the node but also update it
                         val beforeUpdate = node
                         node = updateNodeAndReplaceIfNeeded(prev, next, beforeUpdate)
-                        // if the node is new, we need to run attach on it
-                        attachNeeded = attachNeeded || beforeUpdate !== node
                         logger?.nodeUpdated(i, i, prev, next, beforeUpdate, node)
                     }
                     ActionReuse -> {
@@ -137,10 +135,14 @@
                         // no need to do anything, this is "the same" modifier
                     }
                 }
-                i--
+                // if the node is new, we need to run attach on it
+                if (!node.isAttached) attachNeeded = true
+
                 aggregateChildKindSet = aggregateChildKindSet or node.kindSet
                 node.aggregateChildKindSet = aggregateChildKindSet
+
                 node = node.parent
+                i--
             }
 
             if (i > 0) {
@@ -178,6 +180,18 @@
                 node.aggregateChildKindSet = aggregateChildKindSet
                 i--
             }
+        } else if (after.size == 0) {
+            // common case where we we are removing all the modifiers.
+            var i = before.size - 1
+            // for the linear traversal we want to start with the "unpadded" tail
+            var node: Modifier.Node? = tail.parent
+            while (node != null && i >= 0) {
+                logger?.nodeRemoved(i, before[i], node)
+                val parent = node.parent
+                detachAndRemoveNode(node)
+                node = parent
+                i--
+            }
         } else {
             attachNeeded = true
             coordinatorSyncNeeded = true
@@ -202,6 +216,25 @@
         }
     }
 
+    internal fun resetState() {
+        val current = current
+        if (current == null) {
+            // We have no modifiers set so there is nothing to reset.
+            return
+        }
+        val size = current.size
+        var node: Modifier.Node? = tail.parent
+        var i = size - 1
+        while (node != null && i >= 0) {
+            if (node.isAttached) {
+                node.reset()
+                node.detach()
+            }
+            node = node.parent
+            i--
+        }
+    }
+
     private fun syncCoordinators() {
         var coordinator: NodeCoordinator = innerCoordinator
         var node: Modifier.Node? = tail.parent
@@ -234,7 +267,19 @@
         headToTail {
             if (!it.isAttached) {
                 it.attach()
-                if (performInvalidations) autoInvalidateInsertedNode(it)
+                if (performInvalidations) {
+                    if (it.insertedNodeAwaitingAttachForInvalidation) {
+                        autoInvalidateInsertedNode(it)
+                    }
+                    if (it.updatedNodeAwaitingAttachForInvalidation) {
+                        autoInvalidateUpdatedNode(it)
+                    }
+                }
+                // when we attach with performInvalidations == false no separate
+                // invalidations needed as the whole LayoutNode is attached to the tree.
+                // it will cause all the needed invalidations.
+                it.insertedNodeAwaitingAttachForInvalidation = false
+                it.updatedNodeAwaitingAttachForInvalidation = false
             }
         }
     }
@@ -301,12 +346,14 @@
         var after: MutableVector<Modifier.Element>,
     ) : DiffCallback {
         override fun areItemsTheSame(oldIndex: Int, newIndex: Int): Boolean {
-            return reuseActionForModifiers(before[oldIndex], after[newIndex]) != ActionReplace
+            return actionForModifiers(before[oldIndex], after[newIndex]) != ActionReplace
         }
 
         override fun insert(atIndex: Int, newIndex: Int) {
             val child = node
             node = createAndInsertNodeAsParent(after[newIndex], child)
+            check(!node.isAttached)
+            node.insertedNodeAwaitingAttachForInvalidation = true
             logger?.nodeInserted(atIndex, newIndex, after[newIndex], child, node)
             aggregateChildKindSet = aggregateChildKindSet or node.kindSet
             node.aggregateChildKindSet = aggregateChildKindSet
@@ -315,7 +362,7 @@
         override fun remove(oldIndex: Int) {
             node = node.parent!!
             logger?.nodeRemoved(oldIndex, before[oldIndex], node)
-            node = disposeAndRemoveNode(node)
+            node = detachAndRemoveNode(node)
         }
 
         override fun same(oldIndex: Int, newIndex: Int) {
@@ -427,7 +474,7 @@
         return next
     }
 
-    private fun disposeAndRemoveNode(node: Modifier.Node): Modifier.Node {
+    private fun detachAndRemoveNode(node: Modifier.Node): Modifier.Node {
         if (node.isAttached) {
             // for removing nodes, we always do the autoInvalidateNode call,
             // regardless of whether or not it was a ModifierNodeElement with autoInvalidate
@@ -474,6 +521,8 @@
             }
             else -> BackwardsCompatNode(element)
         }
+        check(!node.isAttached)
+        node.insertedNodeAwaitingAttachForInvalidation = true
         return insertParent(node, child)
     }
 
@@ -503,29 +552,31 @@
     private fun updateNodeAndReplaceIfNeeded(
         prev: Modifier.Element,
         next: Modifier.Element,
-        node: Modifier.Node,
+        node: Modifier.Node
     ): Modifier.Node {
         when {
             prev is ModifierNodeElement<*> && next is ModifierNodeElement<*> -> {
                 val updated = next.updateUnsafe(node)
                 if (updated !== node) {
+                    check(!updated.isAttached)
+                    updated.insertedNodeAwaitingAttachForInvalidation = true
                     // if a new instance is returned, we want to detach the old one
                     if (node.isAttached) {
                         autoInvalidateRemovedNode(node)
                         node.detach()
                     }
-                    val result = replaceNode(node, updated)
-                    if (node.isAttached) {
-                        autoInvalidateInsertedNode(updated)
-                    }
-                    return result
+                    return replaceNode(node, updated)
                 } else {
                     // the node was updated. we are done.
-                    if (next.autoInvalidate && updated.isAttached) {
-                        // the modifier element is labeled as "auto invalidate", which means
-                        // that since the node was updated, we need to invalidate everything
-                        // relevant to it.
-                        autoInvalidateUpdatedNode(updated)
+                    if (next.autoInvalidate) {
+                        if (updated.isAttached) {
+                            // the modifier element is labeled as "auto invalidate", which means
+                            // that since the node was updated, we need to invalidate everything
+                            // relevant to it.
+                            autoInvalidateUpdatedNode(updated)
+                        } else {
+                            updated.updatedNodeAwaitingAttachForInvalidation = true
+                        }
                     }
                     return updated
                 }
@@ -535,6 +586,8 @@
                 // We always autoInvalidate BackwardsCompatNode.
                 if (node.isAttached) {
                     autoInvalidateUpdatedNode(node)
+                } else {
+                    node.updatedNodeAwaitingAttachForInvalidation = true
                 }
                 return node
             }
@@ -661,7 +714,7 @@
  * 2. if modifiers are same class, we REUSE and UPDATE
  * 3. else REPLACE (NO REUSE, NO UPDATE)
  */
-internal fun reuseActionForModifiers(prev: Modifier.Element, next: Modifier.Element): Int {
+internal fun actionForModifiers(prev: Modifier.Element, next: Modifier.Element): Int {
     return if (prev == next)
         ActionReuse
     else if (areObjectsOfSameType(prev, next))
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 01cc2c9..19ff440 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -84,6 +84,8 @@
     override val coordinates: LayoutCoordinates
         get() = this
 
+    private var released = false
+
     private fun headNode(includeTail: Boolean): Modifier.Node? {
         return if (layoutNode.outerCoordinator === this) {
             layoutNode.nodes.head
@@ -158,7 +160,7 @@
         get() = _measureResult != null
 
     override val isAttached: Boolean
-        get() = tail.isAttached
+        get() = !released && layoutNode.isAttached
 
     private var _measureResult: MeasureResult? = null
     override var measureResult: MeasureResult
@@ -926,30 +928,19 @@
     }
 
     /**
-     * Attaches the [NodeCoordinator] and its wrapped [NodeCoordinator] to an active
-     * LayoutNode.
-     *
      * This will be called when the [LayoutNode] associated with this [NodeCoordinator] is
      * attached to the [Owner].
-     *
-     * It is also called whenever the modifier chain is replaced and the [NodeCoordinator]s are
-     * recreated.
      */
-    open fun attach() {
+    fun onLayoutNodeAttach() {
         onLayerBlockUpdated(layerBlock)
     }
 
     /**
-     * Detaches the [NodeCoordinator] and its wrapped [NodeCoordinator] from an active
-     * LayoutNode.
-     *
      * This will be called when the [LayoutNode] associated with this [NodeCoordinator] is
-     * detached from the [Owner].
-     *
-     * It is also called whenever the modifier chain is replaced and the [NodeCoordinator]s are
-     * recreated.
+     * released or when the [NodeCoordinator] is released (will not be used anymore).
      */
-    open fun detach() {
+    fun onRelease() {
+        released = true
         if (layer != null) {
             onLayerBlockUpdated(null)
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 595c6b6..c57a109 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -210,7 +210,7 @@
         node.invalidateMeasurements()
         if (phase == Removed) {
             val coordinator = node.requireCoordinator(Nodes.Layout)
-            coordinator.detach()
+            coordinator.onRelease()
         }
     }
     if (node.isKind(Nodes.GlobalPositionAware) && node is GlobalPositionAwareModifierNode) {
@@ -227,7 +227,9 @@
     }
     if (node.isKind(Nodes.FocusTarget) && node is FocusTargetModifierNode) {
         when (phase) {
-            Removed -> node.onRemoved()
+            // when we previously had focus target modifier on a node and then this modifier
+            // is removed we need to notify the focus tree about so the focus state is reset.
+            Removed -> node.onReset()
             else -> node.requireOwner().focusOwner.scheduleInvalidation(node)
         }
     }
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 84732ff..5b7e198 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -280,9 +280,7 @@
 
     private fun changeModifier(modifier: Modifier) {
         with(layoutNode) {
-            if (isAttached) { forEachNodeCoordinator { it.detach() } }
             this.modifier = modifier
-            if (isAttached) { forEachNodeCoordinator { it.attach() } }
             owner?.onEndApplyChanges()
         }
     }
@@ -314,9 +312,13 @@
             layoutNode: LayoutNode,
             affectsLookahead: Boolean,
             forceRequest: Boolean
-        ) {}
-        override fun onAttach(node: LayoutNode) = node.forEachNodeCoordinator { it.attach() }
-        override fun onDetach(node: LayoutNode) = node.forEachNodeCoordinator { it.detach() }
+        ) {
+        }
+
+        override fun onAttach(node: LayoutNode) =
+            node.forEachNodeCoordinator { it.onLayoutNodeAttach() }
+
+        override fun onDetach(node: LayoutNode) {}
 
         override val root: LayoutNode
             get() = TODO("Not yet implemented")
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index d7fa104..3f54712a 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -2762,12 +2762,11 @@
      * </p>
      *
      * @see AccessibilityEventCompat#getContentChangeTypes for all content change types.
-     * @param minMillisBetweenContentChanges the minimum duration between content change events.
+     * @param duration the minimum duration between content change events.
      */
-    public void setMinMillisBetweenContentChanges(int minMillisBetweenContentChanges) {
+    public void setMinMillisBetweenContentChanges(int duration) {
         if (Build.VERSION.SDK_INT >= 19) {
-            Api19Impl.getExtras(mInfo).putInt(MIN_MILLIS_BETWEEN_CONTENT_CHANGES_KEY,
-                    minMillisBetweenContentChanges);
+            Api19Impl.getExtras(mInfo).putInt(MIN_MILLIS_BETWEEN_CONTENT_CHANGES_KEY, duration);
         }
     }
 
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index c0b0036..9dc8728 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -224,8 +224,8 @@
     @DiagnosticCode public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4;
 
     /**
-     * Indicates that the profile is compressed and a newer version of bundletool needs to be used
-     * to build the app.
+     * Indicates that the profile is compressed and a version of bundletool newer than 1.13.2
+     * needs to be used to build the app.
      */
     @DiagnosticCode public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5;
 
diff --git a/webkit/webkit/build.gradle b/webkit/webkit/build.gradle
index bb84e97..0cc7c77 100644
--- a/webkit/webkit/build.gradle
+++ b/webkit/webkit/build.gradle
@@ -72,4 +72,5 @@
     publish = Publish.SNAPSHOT_AND_RELEASE
     inceptionYear = "2017"
     description = "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
+    additionalDeviceTestApkKeys.add("chrome")
 }