[go: nahoru, domu]

Fix the models reads inside the composition and writes in WithConstrains measure block

During migrating some components to WithConstraints we faced an interesting incorrect behaviour.
There are cases when we modify some model inside onMeasure and then use this updated value inside composition. It works fine without WithConstraints as measuring and composition never were happening in the same frame. Now it is possible as WithConstraints is subcomposing inside the measure block and then performs the measuring, so this operations happens in the same frame. And the recomposition wasn't happening because the write to the model happens in the same frame where we created the model and such cases triggers a bit different and CommitFrameObserver will not be called with such model. To make this possible we should always switch frames and do measuring not in the same frame we do subcomposition.

Test: a test for this use case in WithConstraintsTest
Change-Id: I204fa600b19b7801b4ef20decc3ba10d79b02c10
diff --git a/ui/ui-framework/src/androidTest/java/androidx/ui/core/test/WithConstraintsTest.kt b/ui/ui-framework/src/androidTest/java/androidx/ui/core/test/WithConstraintsTest.kt
index 3d54a3f..8a8f9a4 100644
--- a/ui/ui-framework/src/androidTest/java/androidx/ui/core/test/WithConstraintsTest.kt
+++ b/ui/ui-framework/src/androidTest/java/androidx/ui/core/test/WithConstraintsTest.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.RequiresApi
 import androidx.compose.Composable
 import androidx.compose.memo
+import androidx.compose.state
 import androidx.compose.unaryPlus
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
@@ -395,6 +396,34 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun updateModelInMeasuringAndReadItInCompositionWorksInsideWithConstraints() {
+        val latch = CountDownLatch(1)
+        rule.runOnUiThread {
+            activity.setContentInFrameLayout {
+                Container(width = 100.ipx, height = 100.ipx) {
+                        WithConstraints {
+                            // this replicates the popular pattern we currently use
+                            // where we save some data calculated in the measuring block
+                            // and then use it in the next composition frame
+                            var model by +state { false }
+                            Layout({
+                                if (model) {
+                                    latch.countDown()
+                                }
+                            }) { _, _ ->
+                                if (!model) {
+                                    model = true
+                                }
+                                layout(100.ipx, 100.ipx) {}
+                            }
+                        }
+                }
+            }
+        }
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+    }
+
     private fun takeScreenShot(size: Int): Bitmap {
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
         val bitmap = rule.waitAndScreenShot()
diff --git a/ui/ui-framework/src/main/java/androidx/ui/core/Layout.kt b/ui/ui-framework/src/main/java/androidx/ui/core/Layout.kt
index 09558d0..2c375a1 100644
--- a/ui/ui-framework/src/main/java/androidx/ui/core/Layout.kt
+++ b/ui/ui-framework/src/main/java/androidx/ui/core/Layout.kt
@@ -20,6 +20,7 @@
 import androidx.compose.Compose
 import androidx.compose.CompositionReference
 import androidx.compose.Context
+import androidx.compose.FrameManager
 import androidx.compose.ambient
 import androidx.compose.compositionReference
 import androidx.compose.memo
@@ -481,6 +482,11 @@
             if (lastConstraints != constraints || forceRecompose) {
                 lastConstraints = constraints
                 root.ignoreModelReads { subcompose() }
+                // if there were models created and read inside this subcomposition
+                // and we are going to modify this models within the same frame
+                // the composables which read this model will not be recomposed.
+                // to make this possible we should switch to the next frame.
+                FrameManager.nextFrame()
             }
 
             // Measure the obtained children and compute our size.