[go: nahoru, domu]

Improve AndroidOwner assertions and fix requesting remeasure of the root during the measure pass

I made assertions in AndroidOwner a bit more readable and run our samples containing the complex screens and interactions with this flag enabled. This allows me to find few more cases leading to the inconsistent layout states. This cl improves assertions logic and fixes found issues. All this assertions and fixes should decrease amount of crashes happening because of drawing happening on the layoutnode which is not measured or laid out.

Test: manually on our samples with the assertion flag enabled plus a new test
Bug: 148278782
Change-Id: Ifacb5e1775411c46b3eb6443b72eef606ce78ae5
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 68bb364..3658383 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
@@ -556,6 +556,38 @@
         assertTrue(innerLayoutLatch.await(1, TimeUnit.SECONDS))
     }
 
+    @Test
+    fun triggerRootRemeasureWhileRootIsLayouting() {
+        val latch = CountDownLatch(1)
+        rule.runOnUiThread {
+            activity.setContent {
+                val state = state { 0 }
+                ContainerChildrenAffectsParentSize(100.ipx, 100.ipx) {
+                    WithConstraints {
+                        Layout(children = {
+                            Draw { _, _ ->
+                                latch.countDown()
+                            }
+                        }) { _, _ ->
+                            // read and write once inside measureBlock
+                            if (state.value == 0) {
+                                state.value = 1
+                            }
+                            layout(100.ipx, 100.ipx) {}
+                        }
+                    }
+                    Container(100.ipx, 100.ipx) {
+                        WithConstraints {}
+                    }
+                }
+            }
+        }
+
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+        // before the fix this was failing our internal assertions in AndroidOwner
+        // so nothing else to assert, apart from not crashing
+    }
+
     private fun takeScreenShot(size: Int): Bitmap {
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
         val bitmap = rule.waitAndScreenShot()
@@ -610,6 +642,25 @@
 }
 
 @Composable
+fun ContainerChildrenAffectsParentSize(
+    width: IntPx,
+    height: IntPx,
+    children: @Composable() () -> Unit
+) {
+    Layout(children = children, measureBlock = remember<MeasureBlock>(width, height) {
+        { measurables, _ ->
+            val constraint = Constraints(maxWidth = width, maxHeight = height)
+            val placeables = measurables.map { it.measure(constraint) }
+            layout(width, height) {
+                placeables.forEach {
+                    it.place((width - width) / 2, (height - height) / 2)
+                }
+            }
+        }
+    })
+}
+
+@Composable
 private fun ChangingConstraintsLayout(size: ValueModel<IntPx>, children: @Composable() () -> Unit) {
     Layout(children) { measurables, _ ->
         layout(100.ipx, 100.ipx) {
diff --git a/ui/ui-platform/api/0.1.0-dev04.txt b/ui/ui-platform/api/0.1.0-dev04.txt
index 271e13d..2f76925 100644
--- a/ui/ui-platform/api/0.1.0-dev04.txt
+++ b/ui/ui-platform/api/0.1.0-dev04.txt
@@ -197,8 +197,10 @@
     method public androidx.ui.unit.IntPx getX();
     method public androidx.ui.unit.IntPx getY();
     method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public boolean isLayingOut();
     method public boolean isMeasuring();
     method public boolean isPlaced();
+    method public void layout();
     method public androidx.ui.unit.IntPx maxIntrinsicHeight(androidx.ui.unit.IntPx width);
     method public androidx.ui.unit.IntPx maxIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public androidx.ui.core.Placeable measure(androidx.ui.core.Constraints constraints);
@@ -206,12 +208,9 @@
     method public androidx.ui.unit.IntPx minIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public void onInvalidate();
     method public void place(androidx.ui.unit.IntPx x, androidx.ui.unit.IntPx y);
-    method public void placeChildren();
     method public void requestRemeasure();
-    method public void setAffectsParentSize(boolean p);
     method public void setConstraints(androidx.ui.core.Constraints p);
     method public void setMeasureBlocks(androidx.ui.core.LayoutNode.MeasureBlocks value);
-    method public void setMeasuring(boolean p);
     method public void setModifier(androidx.ui.core.Modifier value);
     method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
     method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
@@ -223,6 +222,7 @@
     property public final androidx.ui.unit.IntPxPosition contentPosition;
     property public final androidx.ui.unit.IntPxSize contentSize;
     property public final androidx.ui.unit.IntPx height;
+    property public final boolean isLayingOut;
     property public final boolean isMeasuring;
     property public final boolean isPlaced;
     property public final java.util.List<androidx.ui.core.LayoutNode> layoutChildren;
diff --git a/ui/ui-platform/api/current.txt b/ui/ui-platform/api/current.txt
index 271e13d..2f76925 100644
--- a/ui/ui-platform/api/current.txt
+++ b/ui/ui-platform/api/current.txt
@@ -197,8 +197,10 @@
     method public androidx.ui.unit.IntPx getX();
     method public androidx.ui.unit.IntPx getY();
     method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public boolean isLayingOut();
     method public boolean isMeasuring();
     method public boolean isPlaced();
+    method public void layout();
     method public androidx.ui.unit.IntPx maxIntrinsicHeight(androidx.ui.unit.IntPx width);
     method public androidx.ui.unit.IntPx maxIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public androidx.ui.core.Placeable measure(androidx.ui.core.Constraints constraints);
@@ -206,12 +208,9 @@
     method public androidx.ui.unit.IntPx minIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public void onInvalidate();
     method public void place(androidx.ui.unit.IntPx x, androidx.ui.unit.IntPx y);
-    method public void placeChildren();
     method public void requestRemeasure();
-    method public void setAffectsParentSize(boolean p);
     method public void setConstraints(androidx.ui.core.Constraints p);
     method public void setMeasureBlocks(androidx.ui.core.LayoutNode.MeasureBlocks value);
-    method public void setMeasuring(boolean p);
     method public void setModifier(androidx.ui.core.Modifier value);
     method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
     method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
@@ -223,6 +222,7 @@
     property public final androidx.ui.unit.IntPxPosition contentPosition;
     property public final androidx.ui.unit.IntPxSize contentSize;
     property public final androidx.ui.unit.IntPx height;
+    property public final boolean isLayingOut;
     property public final boolean isMeasuring;
     property public final boolean isPlaced;
     property public final java.util.List<androidx.ui.core.LayoutNode> layoutChildren;
diff --git a/ui/ui-platform/api/public_plus_experimental_0.1.0-dev04.txt b/ui/ui-platform/api/public_plus_experimental_0.1.0-dev04.txt
index 8f2abf3..6ed203b 100644
--- a/ui/ui-platform/api/public_plus_experimental_0.1.0-dev04.txt
+++ b/ui/ui-platform/api/public_plus_experimental_0.1.0-dev04.txt
@@ -199,8 +199,10 @@
     method public androidx.ui.unit.IntPx getX();
     method public androidx.ui.unit.IntPx getY();
     method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public boolean isLayingOut();
     method public boolean isMeasuring();
     method public boolean isPlaced();
+    method public void layout();
     method public androidx.ui.unit.IntPx maxIntrinsicHeight(androidx.ui.unit.IntPx width);
     method public androidx.ui.unit.IntPx maxIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public androidx.ui.core.Placeable measure(androidx.ui.core.Constraints constraints);
@@ -208,12 +210,9 @@
     method public androidx.ui.unit.IntPx minIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public void onInvalidate();
     method public void place(androidx.ui.unit.IntPx x, androidx.ui.unit.IntPx y);
-    method public void placeChildren();
     method public void requestRemeasure();
-    method public void setAffectsParentSize(boolean p);
     method public void setConstraints(androidx.ui.core.Constraints p);
     method public void setMeasureBlocks(androidx.ui.core.LayoutNode.MeasureBlocks value);
-    method public void setMeasuring(boolean p);
     method public void setModifier(androidx.ui.core.Modifier value);
     method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
     method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
@@ -225,6 +224,7 @@
     property public final androidx.ui.unit.IntPxPosition contentPosition;
     property public final androidx.ui.unit.IntPxSize contentSize;
     property public final androidx.ui.unit.IntPx height;
+    property public final boolean isLayingOut;
     property public final boolean isMeasuring;
     property public final boolean isPlaced;
     property public final java.util.List<androidx.ui.core.LayoutNode> layoutChildren;
diff --git a/ui/ui-platform/api/public_plus_experimental_current.txt b/ui/ui-platform/api/public_plus_experimental_current.txt
index 8f2abf3..6ed203b 100644
--- a/ui/ui-platform/api/public_plus_experimental_current.txt
+++ b/ui/ui-platform/api/public_plus_experimental_current.txt
@@ -199,8 +199,10 @@
     method public androidx.ui.unit.IntPx getX();
     method public androidx.ui.unit.IntPx getY();
     method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public boolean isLayingOut();
     method public boolean isMeasuring();
     method public boolean isPlaced();
+    method public void layout();
     method public androidx.ui.unit.IntPx maxIntrinsicHeight(androidx.ui.unit.IntPx width);
     method public androidx.ui.unit.IntPx maxIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public androidx.ui.core.Placeable measure(androidx.ui.core.Constraints constraints);
@@ -208,12 +210,9 @@
     method public androidx.ui.unit.IntPx minIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public void onInvalidate();
     method public void place(androidx.ui.unit.IntPx x, androidx.ui.unit.IntPx y);
-    method public void placeChildren();
     method public void requestRemeasure();
-    method public void setAffectsParentSize(boolean p);
     method public void setConstraints(androidx.ui.core.Constraints p);
     method public void setMeasureBlocks(androidx.ui.core.LayoutNode.MeasureBlocks value);
-    method public void setMeasuring(boolean p);
     method public void setModifier(androidx.ui.core.Modifier value);
     method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
     method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
@@ -225,6 +224,7 @@
     property public final androidx.ui.unit.IntPxPosition contentPosition;
     property public final androidx.ui.unit.IntPxSize contentSize;
     property public final androidx.ui.unit.IntPx height;
+    property public final boolean isLayingOut;
     property public final boolean isMeasuring;
     property public final boolean isPlaced;
     property public final java.util.List<androidx.ui.core.LayoutNode> layoutChildren;
diff --git a/ui/ui-platform/api/restricted_0.1.0-dev04.txt b/ui/ui-platform/api/restricted_0.1.0-dev04.txt
index 11ff03b..2488424 100644
--- a/ui/ui-platform/api/restricted_0.1.0-dev04.txt
+++ b/ui/ui-platform/api/restricted_0.1.0-dev04.txt
@@ -199,8 +199,10 @@
     method public androidx.ui.unit.IntPx getX();
     method public androidx.ui.unit.IntPx getY();
     method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public boolean isLayingOut();
     method public boolean isMeasuring();
     method public boolean isPlaced();
+    method public void layout();
     method public androidx.ui.unit.IntPx maxIntrinsicHeight(androidx.ui.unit.IntPx width);
     method public androidx.ui.unit.IntPx maxIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public androidx.ui.core.Placeable measure(androidx.ui.core.Constraints constraints);
@@ -208,12 +210,9 @@
     method public androidx.ui.unit.IntPx minIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public void onInvalidate();
     method public void place(androidx.ui.unit.IntPx x, androidx.ui.unit.IntPx y);
-    method public void placeChildren();
     method public void requestRemeasure();
-    method public void setAffectsParentSize(boolean p);
     method public void setConstraints(androidx.ui.core.Constraints p);
     method public void setMeasureBlocks(androidx.ui.core.LayoutNode.MeasureBlocks value);
-    method public void setMeasuring(boolean p);
     method public void setModifier(androidx.ui.core.Modifier value);
     method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
     method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
@@ -225,6 +224,7 @@
     property public final androidx.ui.unit.IntPxPosition contentPosition;
     property public final androidx.ui.unit.IntPxSize contentSize;
     property public final androidx.ui.unit.IntPx height;
+    property public final boolean isLayingOut;
     property public final boolean isMeasuring;
     property public final boolean isPlaced;
     property public final java.util.List<androidx.ui.core.LayoutNode> layoutChildren;
diff --git a/ui/ui-platform/api/restricted_current.txt b/ui/ui-platform/api/restricted_current.txt
index 11ff03b..2488424 100644
--- a/ui/ui-platform/api/restricted_current.txt
+++ b/ui/ui-platform/api/restricted_current.txt
@@ -199,8 +199,10 @@
     method public androidx.ui.unit.IntPx getX();
     method public androidx.ui.unit.IntPx getY();
     method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public boolean isLayingOut();
     method public boolean isMeasuring();
     method public boolean isPlaced();
+    method public void layout();
     method public androidx.ui.unit.IntPx maxIntrinsicHeight(androidx.ui.unit.IntPx width);
     method public androidx.ui.unit.IntPx maxIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public androidx.ui.core.Placeable measure(androidx.ui.core.Constraints constraints);
@@ -208,12 +210,9 @@
     method public androidx.ui.unit.IntPx minIntrinsicWidth(androidx.ui.unit.IntPx height);
     method public void onInvalidate();
     method public void place(androidx.ui.unit.IntPx x, androidx.ui.unit.IntPx y);
-    method public void placeChildren();
     method public void requestRemeasure();
-    method public void setAffectsParentSize(boolean p);
     method public void setConstraints(androidx.ui.core.Constraints p);
     method public void setMeasureBlocks(androidx.ui.core.LayoutNode.MeasureBlocks value);
-    method public void setMeasuring(boolean p);
     method public void setModifier(androidx.ui.core.Modifier value);
     method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
     method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.ui.core.Owner,kotlin.Unit>? p);
@@ -225,6 +224,7 @@
     property public final androidx.ui.unit.IntPxPosition contentPosition;
     property public final androidx.ui.unit.IntPxSize contentSize;
     property public final androidx.ui.unit.IntPx height;
+    property public final boolean isLayingOut;
     property public final boolean isMeasuring;
     property public final boolean isPlaced;
     property public final java.util.List<androidx.ui.core.LayoutNode> layoutChildren;
diff --git a/ui/ui-platform/src/main/java/androidx/ui/core/AndroidOwner.kt b/ui/ui-platform/src/main/java/androidx/ui/core/AndroidOwner.kt
index 7082000..a0bd3f3 100644
--- a/ui/ui-platform/src/main/java/androidx/ui/core/AndroidOwner.kt
+++ b/ui/ui-platform/src/main/java/androidx/ui/core/AndroidOwner.kt
@@ -281,6 +281,9 @@
     override fun onRequestMeasure(layoutNode: LayoutNode) {
         trace("AndroidOwner:onRequestMeasure") {
             layoutNode.requireOwner()
+            if (enableExtraAssertions) {
+                Log.d("AndroidOwner", "onRequestMeasure on $layoutNode")
+            }
             if (layoutNode.isMeasuring) {
                 // we're already measuring it, let's swallow. example when it happens: we compose
                 // DataNode inside WithConstraints, this calls onRequestMeasure on DataNode's
@@ -295,9 +298,8 @@
             // find root of layout request:
             var layout = layoutNode
             while (layout.affectsParentSize && layout.parentLayoutNode != null) {
-                layout.needsRemeasure = true
                 val parent = layout.parentLayoutNode!!
-                if (parent.isMeasuring) {
+                if (parent.isMeasuring || parent.isLayingOut) {
                     if (layout.measureIteration == measureIteration) {
                         // the node we want to remeasure is the child of the parent which is
                         // currently being measured and this parent did already measure us as a
@@ -314,6 +316,7 @@
                     assertLayoutDirtyStateIsConsistent()
                     return
                 } else {
+                    layout.needsRemeasure = true
                     if (parent.needsRemeasure) {
                         // don't need to do anything else since the parent is already scheduled
                         // for a remeasuring
@@ -330,9 +333,10 @@
     }
 
     private fun requestRelayout(layoutNode: LayoutNode) {
-        if (layoutNode.needsRelayout || (layoutNode.needsRemeasure && layoutNode !== root)) {
+        if (layoutNode.needsRelayout || (layoutNode.needsRemeasure && layoutNode !== root) ||
+                layoutNode.isLayingOut) {
             // don't need to do anything else since the parent is already scheduled
-            // for a relayout (measure pass includes relayout).
+            // for a relayout (measure pass includes relayout), or is layouting right now
             assertLayoutDirtyStateIsConsistent()
             return
         }
@@ -367,13 +371,15 @@
                     "means it should only be scheduled for relayouting, not remeasuring unless " +
                     "it`s the root node."
         }
+        if (nodeToRelayout == root) {
+            nodeToRelayout.needsRemeasure = true
+        }
         if (duringMeasureLayout) {
             relayoutNodesDuringMeasureLayout += nodeToRelayout
         } else {
             val noRelayoutScheduled = relayoutNodes.isEmpty()
             relayoutNodes += nodeToRelayout
             if (nodeToRelayout == root || constraints.isZero) {
-                nodeToRelayout.needsRemeasure = true
                 requestLayout()
             } else if (noRelayoutScheduled) {
                 // Invalidate and catch measureAndLayout() in the dispatchDraw()
@@ -444,7 +450,7 @@
                         measureIteration++
                         relayoutNodes.forEach { layoutNode ->
                             if (layoutNode.isAttached()) {
-                                if (layoutNode == root) {
+                                if (layoutNode === root) {
                                     // it is the root node - the only top node from relayoutNodes
                                     // which needs to be remeasured.
                                     layoutNode.measure(constraints)
@@ -454,8 +460,9 @@
                                             "consists of the top nodes of the affected subtrees"
                                 }
                                 if (layoutNode.needsRelayout) {
-                                    layoutNode.placeChildren()
+                                    layoutNode.layout()
                                     onPositionedDispatcher.onNodePositioned(layoutNode)
+                                    assertLayoutDirtyStateIsConsistent()
                                 }
                             }
                         }
@@ -477,9 +484,9 @@
                         }
                     }
                     onPositionedDispatcher.dispatch()
+                    assertLayoutDirtyStateIsConsistent()
                 } finally {
                     duringMeasureLayout = false
-                    assertLayoutDirtyStateIsConsistent()
                 }
             }
             if (!repaintBoundaryChanges.isEmpty()) {
@@ -743,24 +750,41 @@
      */
     private fun assertLayoutDirtyStateIsConsistent() {
         if (enableExtraAssertions) {
-            fun LayoutNode.inconsistentLayoutState(): Boolean {
-                if (needsRelayout || needsRemeasure) {
-                    val parent = parentLayoutNode
-                    return parent != null && isPlaced &&
-                            !relayoutNodes.contains(this) &&
-                            !relayoutNodesDuringMeasureLayout.contains(this) &&
-                            // parent is not yet measured
-                            !parent.needsRemeasure &&
-                            // node is not affecting parent size and parent is not laid out
-                            !(needsRemeasure && parent.needsRelayout && !affectsParentSize) &&
-                            // node and parent both not yet laid out
-                            !(needsRelayout && parent.needsRelayout) &&
-                            // parent is measuring, but didn't yet measure node
-                            (parent.isMeasuring && needsRemeasure &&
-                                    measureIteration != measureIteration)
-                } else {
-                    return false
+            fun LayoutNode.consistentLayoutState(): Boolean {
+                if (this === root && needsRemeasure) {
+                    return relayoutNodes.contains(this) ||
+                            relayoutNodesDuringMeasureLayout.contains(this)
                 }
+                val parent = parentLayoutNode
+                if (parent != null && isPlaced) {
+                    if (needsRelayout) {
+                        if (!relayoutNodes.contains(this) &&
+                            !relayoutNodesDuringMeasureLayout.contains(this)) {
+                            // the parent should also have needsRelayout or it is still
+                            // measuring, this will trigger relayout right after
+                            return parent.needsRelayout || parent.isMeasuring
+                        }
+                    }
+                    if (needsRemeasure) {
+                        if (parent.isMeasuring || parent.isLayingOut) {
+                            return !duringMeasureLayout ||
+                                    parent.measureIteration != measureIteration
+                        } else {
+                            val parentRemeasureScheduled = parent.needsRemeasure ||
+                                    postponedMeasureRequests.contains(parent)
+                            if (affectsParentSize) {
+                                // node and parent both not yet laid out -> parent remeasure
+                                // should be scheduled
+                                return parentRemeasureScheduled
+                            } else {
+                                // node is not affecting parent size and parent relayout(or
+                                // remeasure, as it includes relayout) is scheduled
+                                return parent.needsRelayout || parentRemeasureScheduled
+                            }
+                        }
+                    }
+                }
+                return true
             }
             var inconsistencyFound = false
             logTree { node ->
@@ -771,8 +795,10 @@
                         if (node.needsRelayout) append("[needsRelayout]")
                         if (node.isMeasuring) append("[isMeasuring]")
                         if (duringMeasureLayout) append("[#${node.measureIteration}]")
+                        if (node.isLayingOut) append("[isLayingOut]")
                         if (!node.isPlaced) append("[!isPlaced]")
-                        if (node.inconsistentLayoutState()) {
+                        if (node.affectsParentSize) append("[affectsParentSize]")
+                        if (!node.consistentLayoutState()) {
                             append("[INCONSISTENT]")
                             inconsistencyFound = true
                         }
@@ -789,25 +815,22 @@
 
     /** Prints the nodes tree into the logs. */
     private fun logTree(nodeToString: (ComponentNode) -> String = { it.toString() }) {
-        fun StringBuilder.printSubTree(node: ComponentNode, depth: Int) {
+        fun printSubTree(node: ComponentNode, depth: Int) {
             var childrenDepth = depth
             val nodeRepresentation = nodeToString(node)
             if (nodeRepresentation.isNotEmpty()) {
-                if (isNotEmpty()) {
-                    appendln()
-                }
+                val stringBuilder = StringBuilder()
                 for (i in 0 until depth) {
-                    append("..")
+                    stringBuilder.append("..")
                 }
-                append(nodeRepresentation)
+                stringBuilder.append(nodeRepresentation)
+                Log.d("AndroidOwner", stringBuilder.toString())
                 childrenDepth += 1
             }
             node.visitChildren { printSubTree(it, childrenDepth) }
         }
-        val stringBuilder = StringBuilder()
-        stringBuilder.append("Tree state:")
-        stringBuilder.printSubTree(root, 0)
-        Log.d("AndroidOwner", stringBuilder.toString())
+        Log.d("AndroidOwner", "Tree state:")
+        printSubTree(root, 0)
     }
 
     companion object {
diff --git a/ui/ui-platform/src/main/java/androidx/ui/core/ComponentNodes.kt b/ui/ui-platform/src/main/java/androidx/ui/core/ComponentNodes.kt
index 5ccbd17..743f511 100644
--- a/ui/ui-platform/src/main/java/androidx/ui/core/ComponentNodes.kt
+++ b/ui/ui-platform/src/main/java/androidx/ui/core/ComponentNodes.kt
@@ -983,11 +983,19 @@
      * `true` when the parent's size depends on this LayoutNode's size
      */
     var affectsParentSize: Boolean = true
+        private set
 
     /**
      * `true` when inside [measure]
      */
     var isMeasuring: Boolean = false
+        private set
+
+    /**
+     * `true` when inside [layout]
+     */
+    var isLayingOut: Boolean = false
+        private set
 
     /**
      * `true` when the current node is positioned during the measure pass,
@@ -1002,6 +1010,7 @@
     var needsRemeasure = false
         internal set(value) {
             require(!isMeasuring)
+            require(!isLayingOut)
             field = value
         }
 
@@ -1010,7 +1019,11 @@
      * lambda accessed a model that has been dirtied.
      */
     var needsRelayout = false
-        internal set
+        internal set(value) {
+            require(!isMeasuring)
+            require(!isLayingOut)
+            field = value
+        }
 
     /**
      * `true` when the parent reads our alignment lines
@@ -1249,7 +1262,7 @@
             if (oldContentPosition != contentPosition) {
                 owner?.onPositionChange(this@LayoutNode)
             }
-            placeChildren()
+            layout()
         }
 
         override val density: Density get() = measureScope.density
@@ -1478,20 +1491,16 @@
             "measure() may not be called multiple times on the same Measurable"
         }
         measureIteration = iteration
+        val parent = parentLayoutNode
+        // The more idiomatic, `if (parentLayoutNode?.isMeasuring == true)` causes boxing
+        affectsParentSize = parent != null && parent.isMeasuring == true
         if (this.constraints == constraints && !needsRemeasure) {
-            val parent = parentLayoutNode
-            if (parent != null && parent.isMeasuring) {
-                affectsParentSize = true
-            }
             return layoutNodeWrapper // we're already measured to this size, don't do anything
         }
 
         needsRemeasure = false
         isMeasuring = true
         dirtyAlignmentLines = true
-        layoutChildren.forEach { child ->
-            child.affectsParentSize = false
-        }
         this.constraints = constraints
         owner.observeMeasureModelReads(this) {
             layoutNodeWrapper.measure(constraints)
@@ -1521,8 +1530,10 @@
 
     fun draw(canvas: Canvas, density: Density) = layoutNodeWrapper.draw(canvas, density)
 
-    fun placeChildren() {
+    fun layout() {
         if (needsRelayout) {
+            needsRelayout = false
+            isLayingOut = true
             val owner = requireOwner()
             owner.observeLayoutModelReads(this) {
                 layoutChildren.forEach { child ->
@@ -1541,7 +1552,6 @@
                     child.alignmentLinesRead = child.alignmentLinesQueriedSinceLastLayout
                 }
             }
-            needsRelayout = false
 
             if (alignmentLinesRequired && dirtyAlignmentLines) {
                 alignmentLines.clear()
@@ -1565,6 +1575,7 @@
                 alignmentLines += providedAlignmentLines
                 dirtyAlignmentLines = false
             }
+            isLayingOut = false
         }
     }
 
@@ -1574,7 +1585,7 @@
         alignmentLinesQueriedSinceLastLayout = true
         if (dirtyAlignmentLines) {
             needsRelayout = true
-            placeChildren()
+            layout()
         }
         return alignmentLines
     }
@@ -1584,12 +1595,6 @@
             IntPxSize(layoutResult.width, layoutResult.height)
         )
 
-        // The more idiomatic, `if (parentLayoutNode?.isMeasuring == true)` causes boxing
-        val parent = parentLayoutNode
-        @Suppress("SimplifyBooleanWithConstants")
-        if (parent != null && parent.isMeasuring == true) {
-            affectsParentSize = true
-        }
         if (layoutNodeWrapper.hasDirtySize()) {
             owner?.onSizeChange(this@LayoutNode)
         }