Repository: cashapp/redwood
Pull request #372 (2022-09-16 09:11:52)

Create multiplatform FlexboxEngine implementation.
This PR introduces a `FlexboxEngine` that supports measuring and laying out "nodes" with support for most [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) properties. The code is a port of Google's [Flexbox-layout](https://github.com/google/flexbox-layout) with significant changes to remove all Android/JVM dependencies.

The plan is to use `FlexboxEngine` as an implementation dependency in a future PR to add support for multiplatform [`Column` and `Row` implementations](https://github.com/cashapp/redwood/issues/328).

I still need to clean up references to views and add **a lot** more tests to this PR, but wanted to post this for early feedback.

swankjesse: @JakeWharton Good point on the benefit of having explicit constants. I’m mostly torn between the discoverability benefits of `Undefined` vs. the pervasive pattern of `-1` to mean an undefined `Int`. When I see `Undefined` I’m inclined so assume there’s gonna be a bunch of other magic values like [WRAP_CONTENT](https://developer.android.com/reference/android/view/ViewGroup.LayoutParams#WRAP_CONTENT). If there’s only ever gonna be one special value, -1 is nice.
swankjesse: And yes, FlexboxEngine doesn’t get much value from having an interface.
colinrtwhite: Re: constants discussion I think it's worthwhile to keep `DefaultFlexGrow` and the others since those are mandated defaults by the flexbox spec. Semi-related, but Compose UI defines `DefaultAlpha=1.0`, which imo is useful since I don't need to remember the constant's value. I'm going to inline `Undefined`, though since that's specific to the implementation and -1 is a common default for not set properties.

Patch:
diff --git a/build.gradle b/build.gradle
index b4d3fafb7..218e329ac 100644
--- a/build.gradle
+++ b/build.gradle
@@ -89,6 +89,9 @@ allprojects {
       checkDependencies true
       checkReleaseBuilds false // Full lint runs as part of 'build' task.
     }
+    android.composeOptions {
+      kotlinCompilerExtensionVersion libs.versions.androidxComposeCompiler.get()
+    }
   }
 
   plugins.withId('com.vanniktech.maven.publish.base') {
diff --git a/redwood-flexbox-engine/build.gradle b/redwood-flexbox-engine/build.gradle
new file mode 100644
index 000000000..f0709df32
--- /dev/null
+++ b/redwood-flexbox-engine/build.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'org.jetbrains.kotlin.multiplatform'
+apply plugin: 'com.vanniktech.maven.publish'
+apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.
+
+kotlin {
+  apply from: "${rootDir}/addAllTargets.gradle"
+
+  sourceSets {
+    commonTest {
+      dependencies {
+        implementation libs.kotlin.test
+      }
+    }
+  }
+}
+
+spotless {
+  kotlin {
+    targetExclude(
+      // Apache 2-licensed files from Google.
+      "src/commonMain/kotlin/app/cash/redwood/FlexboxEngine.kt",
+      "src/commonMain/kotlin/app/cash/redwood/Line.kt",
+      "src/commonMain/kotlin/app/cash/redwood/Node.kt",
+    )
+  }
+}
diff --git a/redwood-flexbox-engine/gradle.properties b/redwood-flexbox-engine/gradle.properties
new file mode 100644
index 000000000..ea0c9eeb2
--- /dev/null
+++ b/redwood-flexbox-engine/gradle.properties
@@ -0,0 +1,2 @@
+POM_ARTIFACT_ID=redwood-flexbox-engine
+POM_NAME=Redwood flexbox engine
diff --git a/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/FlexboxEngine.kt b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/FlexboxEngine.kt
new file mode 100644
index 000000000..87415103a
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/FlexboxEngine.kt
@@ -0,0 +1,2041 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * 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:Suppress("MemberVisibilityCanBePrivate")
+
+package app.cash.redwood
+
+import app.cash.redwood.Node.Companion.DefaultFlexBasisPercent
+import app.cash.redwood.Node.Companion.DefaultFlexGrow
+import app.cash.redwood.Node.Companion.MatchParent
+import app.cash.redwood.Node.Companion.UndefinedFlexShrink
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/**
+ * A class that measures and positions its children according to its flexbox properties.
+ */
+public class FlexboxEngine {
+
+  /**
+   * The flex direction attribute of the flexbox.
+   */
+  public var flexDirection: FlexDirection = FlexDirection.Row
+
+  /**
+   * The flex wrap attribute of the flexbox.
+   */
+  public var flexWrap: FlexWrap = FlexWrap.NoWrap
+
+  /**
+   * The justify content attribute of the flexbox.
+   */
+  public var justifyContent: JustifyContent = JustifyContent.FlexStart
+
+  /**
+   * The align content attribute of the flexbox.
+   */
+  public var alignItems: AlignItems = AlignItems.FlexStart
+
+  /**
+   * The align items attribute of the flexbox.
+   */
+  public var alignContent: AlignContent = AlignContent.FlexStart
+
+  /**
+   * The padding of the flexbox.
+   */
+  public var padding: Spacing = Spacing.Zero
+
+  /**
+   * The current value of the maximum number of flex lines. If not set, -1 is returned.
+   */
+  public var maxLines: Int = -1
+
+  /**
+   * Returns the nodes contained in the flexbox.
+   */
+  public val nodes: MutableList<Node> = ObservableMutableList(
+    onChange = { indexToReorderedIndex = null },
+  )
+
+  /**
+   * The computed flex lines after calling [measure].
+   */
+  internal var flexLines = listOf<Line>()
+
+  /**
+   * Holds the reordered indices after [Node.order] has been taken into account.
+   */
+  private var indexToReorderedIndex: IntArray? = null
+
+  /**
+   * Holds the 'frozen' state of children during measure. If a view is frozen it will no longer
+   * expand or shrink regardless of flex grow/flex shrink attributes.
+   */
+  private var childrenFrozen: BooleanArray? = null
+
+  /**
+   * Map the view index to the flex line which contains the view represented by the index to
+   * look for a flex line from a given view index in a constant time.
+   * Key: index of the view
+   * Value: index of the flex line that contains the given view
+   *
+   * E.g. if we have following flex lines,
+   *
+   * FlexLine(0): itemCount 3
+   * FlexLine(1): itemCount 2
+   *
+   * this instance should have following entries
+   *
+   * [0, 0, 0, 1, 1, ...]
+   */
+  internal var indexToFlexLine: IntArray? = null
+
+  /**
+   * Cache the measured spec. The first 32 bit represents the height measure spec, the last
+   * 32 bit represents the width measure spec of each flex item.
+   * E.g. an entry is created like `(long) heightMeasureSpec << 32 | widthMeasureSpec`
+   *
+   * To retrieve a widthMeasureSpec, call [unpackLower] or [unpackHigher] for a heightMeasureSpec.
+   */
+  private var measureSpecCache: LongArray? = null
+
+  /**
+   * Cache a flex item's measured width and height. The first 32 bit represents the height, the
+   * last 32 bit represents the width of each flex item.
+   * E.g. an entry is created like `(long) view.measuredHeight << 32 | view.measuredWidth`
+   *
+   * To retrieve a width value, call [unpackLower] or [unpackHigher] for a height value.
+   */
+  private var measuredSizeCache: LongArray? = null
+
+  /**
+   * Calculate how many flex lines are needed in the flex container.
+   * This method should calculate all the flex lines from the existing flex items.
+   */
+  internal fun calculateHorizontalFlexLines(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+  ) = calculateFlexLines(
+    mainMeasureSpec = widthMeasureSpec,
+    crossMeasureSpec = heightMeasureSpec,
+    needsCalcAmount = Int.MAX_VALUE,
+    fromIndex = 0,
+    toIndex = -1,
+  )
+
+  /**
+   * Calculate how many flex lines are needed in the flex container.
+   * This method should calculate all the flex lines from the existing flex items.
+   */
+  internal fun calculateVerticalFlexLines(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+  ) = calculateFlexLines(
+    mainMeasureSpec = heightMeasureSpec,
+    crossMeasureSpec = widthMeasureSpec,
+    needsCalcAmount = Int.MAX_VALUE,
+    fromIndex = 0,
+    toIndex = -1,
+  )
+
+  /**
+   * Calculates how many flex lines are needed in the flex container layout by measuring each
+   * child.
+   * Expanding or shrinking the flex items depending on the flex grow and flex shrink
+   * attributes are done in a later procedure, so the views' measured width and measured
+   * height may be changed in a later process.
+   *
+   * @param mainMeasureSpec the main axis measure spec imposed by the flex container,
+   * width for horizontal direction, height otherwise
+   * @param crossMeasureSpec the cross axis measure spec imposed by the flex container,
+   * height for horizontal direction, width otherwise
+   * @param needsCalcAmount the amount of pixels where flex line calculation should be stopped
+   * this is needed to avoid the expensive calculation if the
+   * calculation is needed only the small part of the entire flex container.
+   * @param fromIndex the index of the child from which the calculation starts
+   * @param toIndex the index of the child to which the calculation ends (until the
+   * flex line which include the which who has that index). If this
+   * and needsCalcAmount are both set, first flex lines are calculated
+   * to the index, calculate the amount of pixels as the needsCalcAmount
+   * argument in addition to that
+   */
+  @Suppress("SameParameterValue")
+  private fun calculateFlexLines(
+    mainMeasureSpec: MeasureSpec,
+    crossMeasureSpec: MeasureSpec,
+    needsCalcAmount: Int,
+    fromIndex: Int,
+    toIndex: Int,
+  ): List<Line> {
+    val isMainAxisHorizontal = flexDirection.isMainAxisHorizontal
+    val mainMode = mainMeasureSpec.mode
+    val mainSize = mainMeasureSpec.size
+    val flexLines = mutableListOf<Line>()
+    var reachedToIndex = toIndex == -1
+    val (mainPaddingStart, mainPaddingEnd, crossPaddingStart, crossPaddingEnd) = getPadding(isMainAxisHorizontal)
+    var largestSizeInCross = Int.MIN_VALUE
+
+    // The amount of cross size calculated in this method call.
+    var sumCrossSize = 0
+
+    // The index of the view in the flex line.
+    var indexInFlexLine = 0
+    var flexLine = Line()
+    flexLine.firstIndex = fromIndex
+    flexLine.mainSize = mainPaddingStart + mainPaddingEnd
+    val childCount = nodes.size
+    for (i in fromIndex until childCount) {
+      val child = getReorderedChildAt(i)
+      if (child == null) {
+        if (isLastFlexItem(i, childCount, flexLine)) {
+          addFlexLine(flexLines, flexLine, i, sumCrossSize)
+        }
+        continue
+      } else if (!child.visible) {
+        flexLine.invisibleItemCount++
+        flexLine.itemCount++
+        if (isLastFlexItem(i, childCount, flexLine)) {
+          addFlexLine(flexLines, flexLine, i, sumCrossSize)
+        }
+        continue
+      }
+      if (child.alignSelf == AlignSelf.Stretch) {
+        flexLine.indicesAlignSelfStretch += i
+      }
+      var childMainSize = getFlexItemSizeMain(child, isMainAxisHorizontal)
+      if (child.flexBasisPercent != DefaultFlexBasisPercent && mainMode == MeasureSpecMode.Exactly) {
+        childMainSize = (mainSize * child.flexBasisPercent).roundToInt()
+        // Use the dimension from the layout if the mainMode is not
+        // MeasureSpecMode.Exactly even if any fraction value is set to
+        // layout_flexBasisPercent.
+      }
+      var childMainMeasureSpec: MeasureSpec
+      var childCrossMeasureSpec: MeasureSpec
+      if (isMainAxisHorizontal) {
+        childMainMeasureSpec = MeasureSpec.getChildMeasureSpec(
+          spec = mainMeasureSpec,
+          padding = mainPaddingStart + mainPaddingEnd +
+            getFlexItemMarginStartMain(child, true) +
+            getFlexItemMarginEndMain(child, true),
+          childDimension = childMainSize,
+        )
+        childCrossMeasureSpec = MeasureSpec.getChildMeasureSpec(
+          spec = crossMeasureSpec,
+          padding = crossPaddingStart + crossPaddingEnd +
+            getFlexItemMarginStartCross(child, true) +
+            getFlexItemMarginEndCross(child, true) + sumCrossSize,
+          childDimension = getFlexItemSizeCross(child, true),
+        )
+        child.measure(childMainMeasureSpec, childCrossMeasureSpec)
+        updateMeasureCache(i, childMainMeasureSpec, childCrossMeasureSpec, child)
+      } else {
+        childCrossMeasureSpec = MeasureSpec.getChildMeasureSpec(
+          spec = crossMeasureSpec,
+          padding = crossPaddingStart + crossPaddingEnd +
+            getFlexItemMarginStartCross(child, false) +
+            getFlexItemMarginEndCross(child, false) + sumCrossSize,
+          childDimension = getFlexItemSizeCross(child, false),
+        )
+        childMainMeasureSpec = MeasureSpec.getChildMeasureSpec(
+          spec = mainMeasureSpec,
+          padding = mainPaddingStart + mainPaddingEnd +
+            getFlexItemMarginStartMain(child, false) +
+            getFlexItemMarginEndMain(child, false),
+          childDimension = childMainSize,
+        )
+        child.measure(childCrossMeasureSpec, childMainMeasureSpec)
+        updateMeasureCache(i, childCrossMeasureSpec, childMainMeasureSpec, child)
+      }
+
+      // Check the size constraint after the first measurement for the child
+      // To prevent the child's width/height violate the size constraints imposed by the
+      // {@link Node#getMinWidth()}, {@link Node#getMinHeight()},
+      // {@link Node#getMaxWidth()} and {@link Node#getMaxHeight()} attributes.
+      // E.g. When the child's layout_width is wrap_content the measured width may be
+      // less than the min width after the first measurement.
+      checkSizeConstraints(child, i)
+      if (isWrapRequired(
+          mode = mainMode,
+          maxSize = mainSize,
+          currentLength = flexLine.mainSize,
+          childLength = getViewMeasuredSizeMain(child, isMainAxisHorizontal) +
+            getFlexItemMarginStartMain(child, isMainAxisHorizontal) +
+            getFlexItemMarginEndMain(child, isMainAxisHorizontal),
+          flexItem = child,
+          flexLinesSize = flexLines.size,
+        )
+      ) {
+        if (flexLine.itemCountVisible > 0) {
+          addFlexLine(flexLines, flexLine, if (i > 0) i - 1 else 0, sumCrossSize)
+          sumCrossSize += flexLine.crossSize
+        }
+        if (isMainAxisHorizontal) {
+          if (child.height == MatchParent) {
+            // This case takes care of the corner case where the cross size of the
+            // child is affected by the just added flex line.
+            // E.g. when the child's layout_height is set to match_parent, the height
+            // of that child needs to be determined taking the total cross size used
+            // so far into account. In that case, the height of the child needs to be
+            // measured again note that we don't need to judge if the wrapping occurs
+            // because it doesn't change the size along the main axis.
+            childCrossMeasureSpec = MeasureSpec.getChildMeasureSpec(
+              spec = crossMeasureSpec,
+              padding = padding.top + padding.bottom + child.margin.top +
+                child.margin.bottom + sumCrossSize,
+              childDimension = child.height,
+            )
+            child.measure(childMainMeasureSpec, childCrossMeasureSpec)
+            checkSizeConstraints(child, i)
+          }
+        } else {
+          if (child.width == MatchParent) {
+            // This case takes care of the corner case where the cross size of the
+            // child is affected by the just added flex line.
+            // E.g. when the child's layout_width is set to match_parent, the width
+            // of that child needs to be determined taking the total cross size used
+            // so far into account. In that case, the width of the child needs to be
+            // measured again note that we don't need to judge if the wrapping occurs
+            // because it doesn't change the size along the main axis.
+            childCrossMeasureSpec = MeasureSpec.getChildMeasureSpec(
+              spec = crossMeasureSpec,
+              padding = padding.start + padding.end + child.margin.start +
+                child.margin.end + sumCrossSize,
+              childDimension = child.width,
+            )
+            child.measure(childCrossMeasureSpec, childMainMeasureSpec)
+            checkSizeConstraints(child, i)
+          }
+        }
+        flexLine = Line()
+        flexLine.itemCount = 1
+        flexLine.mainSize = mainPaddingStart + mainPaddingEnd
+        flexLine.firstIndex = i
+        indexInFlexLine = 0
+        largestSizeInCross = Int.MIN_VALUE
+      } else {
+        flexLine.itemCount++
+        indexInFlexLine++
+      }
+      flexLine.anyItemsHaveFlexGrow = flexLine.anyItemsHaveFlexGrow ||
+        (child.flexGrow != DefaultFlexGrow)
+      flexLine.anyItemsHaveFlexShrink = flexLine.anyItemsHaveFlexShrink ||
+        (child.flexShrink != UndefinedFlexShrink)
+      if (indexToFlexLine != null) {
+        indexToFlexLine!![i] = flexLines.size
+      }
+      flexLine.mainSize += (
+        getViewMeasuredSizeMain(child, isMainAxisHorizontal) +
+          getFlexItemMarginStartMain(child, isMainAxisHorizontal) +
+          getFlexItemMarginEndMain(child, isMainAxisHorizontal)
+        )
+      flexLine.totalFlexGrow += child.flexGrow
+      flexLine.totalFlexShrink += child.flexShrink
+      largestSizeInCross = max(
+        largestSizeInCross,
+        getViewMeasuredSizeCross(child, isMainAxisHorizontal) +
+          getFlexItemMarginStartCross(child, isMainAxisHorizontal) +
+          getFlexItemMarginEndCross(child, isMainAxisHorizontal),
+      )
+      // Temporarily set the cross axis length as the largest child in the flexLine
+      // Expand along the cross axis depending on the alignContent property if needed
+      // later
+      flexLine.crossSize = max(flexLine.crossSize, largestSizeInCross)
+      if (isMainAxisHorizontal) {
+        if (flexWrap != FlexWrap.WrapReverse) {
+          flexLine.maxBaseline = max(
+            flexLine.maxBaseline,
+            child.baseline + child.margin.top,
+          )
+        } else {
+          // if the flex wrap property is WRAP_REVERSE, calculate the
+          // baseline as the distance from the cross end and the baseline
+          // since the cross size calculation is based on the distance from the cross end
+          flexLine.maxBaseline = max(
+            flexLine.maxBaseline,
+            child.measuredHeight - child.baseline + child.margin.bottom,
+          )
+        }
+      }
+      if (isLastFlexItem(i, childCount, flexLine)) {
+        addFlexLine(flexLines, flexLine, i, sumCrossSize)
+        sumCrossSize += flexLine.crossSize
+      }
+      if (toIndex != -1 && flexLines.size > 0 &&
+        flexLines[flexLines.size - 1].lastIndex >= toIndex &&
+        i >= toIndex && !reachedToIndex
+      ) {
+        // Calculated to include a flex line which includes the flex item having the
+        // toIndex.
+        // Let the sumCrossSize start from the negative value of the last flex line's
+        // cross size because otherwise flex lines aren't calculated enough to fill the
+        // visible area.
+        sumCrossSize = -flexLine.crossSize
+        reachedToIndex = true
+      }
+      if (sumCrossSize > needsCalcAmount && reachedToIndex) {
+        // Stop the calculation if the sum of cross size calculated reached to the point
+        // beyond the needsCalcAmount value to avoid unneeded calculation in a
+        // RecyclerView.
+        // To be precise, the decoration length may be added to the sumCrossSize,
+        // but we omit adding the decoration length because even without the decorator
+        // length, it's guaranteed that calculation is done at least beyond the
+        // needsCalcAmount
+        break
+      }
+    }
+    return flexLines
+  }
+
+  /**
+   * Returns an array with container's padding in the format: [mainStart, mainEnd, crossStart, crossEnd].
+   */
+  private fun getPadding(isMainAxisHorizontal: Boolean): IntArray {
+    return if (isMainAxisHorizontal) {
+      intArrayOf(padding.start, padding.end, padding.top, padding.bottom)
+    } else {
+      intArrayOf(padding.top, padding.bottom, padding.start, padding.end)
+    }
+  }
+
+  /**
+   * Returns the view's measured size in the main axis. Either width or height.
+   *
+   * @param node the view
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the view's measured size in the main axis
+   */
+  private fun getViewMeasuredSizeMain(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.measuredWidth else node.measuredHeight
+  }
+
+  /**
+   * Returns the view's measured size in the cross axis. Either width or height.
+   *
+   * @param node the view
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the view's measured size in the cross axis
+   */
+  private fun getViewMeasuredSizeCross(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.measuredHeight else node.measuredWidth
+  }
+
+  /**
+   * Returns the flexItem's size in the main axis. Either width or height.
+   *
+   * @param node the flexItem
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the flexItem's size in the main axis
+   */
+  private fun getFlexItemSizeMain(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.width else node.height
+  }
+
+  /**
+   * Returns the flexItem's size in the cross axis. Either width or height.
+   *
+   * @param node the flexItem
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the flexItem's size in the cross axis
+   */
+  private fun getFlexItemSizeCross(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.height else node.width
+  }
+
+  /**
+   * Returns the flexItem's start margin in the main axis. Either start or top.
+   *
+   *
+   * @param node the flexItem
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the flexItem's start margin in the main axis
+   */
+  private fun getFlexItemMarginStartMain(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.margin.start else node.margin.top
+  }
+
+  /**
+   * Returns the flexItem's end margin in the main axis. Either end or bottom.
+   *
+   * @param node the flexItem
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the flexItem's end margin in the main axis
+   */
+  private fun getFlexItemMarginEndMain(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.margin.end else node.margin.bottom
+  }
+
+  /**
+   * Returns the flexItem's start margin in the cross axis. Either start or top.
+   *
+   * @param node the flexItem
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the flexItem's start margin in the cross axis
+   */
+  private fun getFlexItemMarginStartCross(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.margin.top else node.margin.start
+  }
+
+  /**
+   * Returns the flexItem's end margin in the cross axis. Either end or bottom.
+   *
+   * @param node the flexItem
+   * @param isMainHorizontal is the main axis horizontal
+   * @return the flexItem's end margin in the cross axis
+   */
+  private fun getFlexItemMarginEndCross(node: Node, isMainHorizontal: Boolean): Int {
+    return if (isMainHorizontal) node.margin.bottom else node.margin.end
+  }
+
+  /**
+   * Determine if a wrap is required (add a new flex line).
+   *
+   * @param mode the width or height mode along the main axis direction
+   * @param maxSize the max size along the main axis direction
+   * @param currentLength the accumulated current length
+   * @param childLength the length of a child view which is to be collected to the flex line
+   * @param flexItem the LayoutParams for the view being determined whether a new flex line is needed
+   * @param flexLinesSize the number of the existing flexlines size
+   * @return `true` if a wrap is required, `false` otherwise
+   */
+  private fun isWrapRequired(
+    mode: MeasureSpecMode,
+    maxSize: Int,
+    currentLength: Int,
+    childLength: Int,
+    flexItem: Node,
+    flexLinesSize: Int,
+  ): Boolean {
+    if (flexWrap == FlexWrap.NoWrap) {
+      return false
+    }
+    if (flexItem.wrapBefore) {
+      return true
+    }
+    if (mode == MeasureSpecMode.Unspecified) {
+      return false
+    }
+    // Judge the condition by adding 1 to the current flexLinesSize because the flex line
+    // being computed isn't added to the flexLinesSize.
+    if (maxLines != -1 && maxLines <= flexLinesSize + 1) {
+      return false
+    }
+    return maxSize < currentLength + childLength
+  }
+
+  private fun isLastFlexItem(
+    childIndex: Int,
+    childCount: Int,
+    flexLine: Line,
+  ) = childIndex == childCount - 1 && flexLine.itemCountVisible > 0
+
+  private fun addFlexLine(
+    flexLines: MutableList<Line>,
+    flexLine: Line,
+    viewIndex: Int,
+    usedCrossSizeSoFar: Int,
+  ) {
+    flexLine.sumCrossSizeBefore = usedCrossSizeSoFar
+    flexLine.lastIndex = viewIndex
+    flexLines.add(flexLine)
+  }
+
+  /**
+   * Checks if the view's width/height don't violate the minimum/maximum size constraints imposed
+   * by the [Node.minWidth], [Node.minHeight], [Node.maxWidth] and [Node.maxHeight] attributes.
+   *
+   * @param node the view to be checked
+   * @param index index of the view
+   */
+  private fun checkSizeConstraints(node: Node, index: Int) {
+    var needsMeasure = false
+    var childWidth = node.measuredWidth
+    var childHeight = node.measuredHeight
+    if (childWidth < node.minWidth) {
+      needsMeasure = true
+      childWidth = node.minWidth
+    } else if (childWidth > node.maxWidth) {
+      needsMeasure = true
+      childWidth = node.maxWidth
+    }
+    if (childHeight < node.minHeight) {
+      needsMeasure = true
+      childHeight = node.minHeight
+    } else if (childHeight > node.maxHeight) {
+      needsMeasure = true
+      childHeight = node.maxHeight
+    }
+    if (needsMeasure) {
+      val widthSpec = MeasureSpec.from(childWidth, MeasureSpecMode.Exactly)
+      val heightSpec = MeasureSpec.from(childHeight, MeasureSpecMode.Exactly)
+      node.measure(widthSpec, heightSpec)
+      updateMeasureCache(index, widthSpec, heightSpec, node)
+    }
+  }
+
+  /**
+   * Determine the main size by expanding (shrinking if negative remaining free space is given)
+   * an individual child in each flex line if any children's flexGrow (or flexShrink if remaining
+   * space is negative) properties are set to non-zero.
+   *
+   * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+   * @param heightMeasureSpec vertical space requirements as imposed by the parent
+   */
+  internal fun determineMainSize(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+    fromIndex: Int = 0,
+  ) {
+    ensureChildrenFrozen(nodes.size)
+    if (fromIndex >= nodes.size) {
+      return
+    }
+    val mainSize: Int
+    val paddingAlongMainAxis: Int
+    when (flexDirection) {
+      FlexDirection.Row, FlexDirection.RowReverse -> {
+        val widthMode = widthMeasureSpec.mode
+        val widthSize = widthMeasureSpec.size
+        val largestMainSize = getLargestMainSize()
+        mainSize = if (widthMode == MeasureSpecMode.Exactly) {
+          widthSize
+        } else {
+          min(largestMainSize, widthSize)
+        }
+        paddingAlongMainAxis = (padding.start + padding.end)
+      }
+      FlexDirection.Column, FlexDirection.ColumnReverse -> {
+        val heightMode = heightMeasureSpec.mode
+        val heightSize = heightMeasureSpec.size
+        mainSize = if (heightMode == MeasureSpecMode.Exactly) {
+          heightSize
+        } else {
+          getLargestMainSize()
+        }
+        paddingAlongMainAxis = (padding.top + padding.bottom)
+      }
+      else -> error("Invalid FlexDirection: $flexDirection")
+    }
+    var flexLineIndex = 0
+    if (indexToFlexLine != null) {
+      flexLineIndex = indexToFlexLine!![fromIndex]
+    }
+    var i = flexLineIndex
+    val size = flexLines.size
+    while (i < size) {
+      val flexLine = flexLines[i]
+      if (flexLine.mainSize < mainSize && flexLine.anyItemsHaveFlexGrow) {
+        expandFlexItems(
+          widthMeasureSpec = widthMeasureSpec,
+          heightMeasureSpec = heightMeasureSpec,
+          flexLine = flexLine,
+          maxMainSize = mainSize,
+          paddingAlongMainAxis = paddingAlongMainAxis,
+          calledRecursively = false,
+        )
+      } else if (flexLine.mainSize > mainSize && flexLine.anyItemsHaveFlexShrink) {
+        shrinkFlexItems(
+          widthMeasureSpec = widthMeasureSpec,
+          heightMeasureSpec = heightMeasureSpec,
+          flexLine = flexLine,
+          maxMainSize = mainSize,
+          paddingAlongMainAxis = paddingAlongMainAxis,
+          calledRecursively = false,
+        )
+      }
+      i++
+    }
+  }
+
+  private fun ensureChildrenFrozen(size: Int) {
+    if (childrenFrozen == null) {
+      childrenFrozen = BooleanArray(max(size, 10))
+    } else if (childrenFrozen!!.size < size) {
+      val newCapacity = childrenFrozen!!.size * 2
+      childrenFrozen = BooleanArray(max(newCapacity, size))
+    } else {
+      childrenFrozen!!.fill(false)
+    }
+  }
+
+  /**
+   * Expand the flex items along the main axis based on the individual [Node.flexGrow] attribute.
+   *
+   * @param widthMeasureSpec the horizontal space requirements as imposed by the parent
+   * @param heightMeasureSpec the vertical space requirements as imposed by the parent
+   * @param flexLine the flex line to which flex items belong
+   * @param maxMainSize the maximum main size. Expanded main size will be this size
+   * @param paddingAlongMainAxis the padding value along the main axis
+   * @param calledRecursively true if this method is called recursively, false otherwise
+   */
+  private fun expandFlexItems(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+    flexLine: Line,
+    maxMainSize: Int,
+    paddingAlongMainAxis: Int,
+    calledRecursively: Boolean,
+  ) {
+    if (flexLine.totalFlexGrow <= 0 || maxMainSize < flexLine.mainSize) {
+      return
+    }
+    val sizeBeforeExpand = flexLine.mainSize
+    var needsReexpand = false
+    val unitSpace = (maxMainSize - flexLine.mainSize) / flexLine.totalFlexGrow
+    flexLine.mainSize = paddingAlongMainAxis
+
+    // Setting the cross size of the flex line as the temporal value since the cross size of
+    // each flex item may be changed from the initial calculation
+    // (in the measureHorizontal/measureVertical method) even this method is part of the main
+    // size determination.
+    // E.g. If a TextView's layout_width is set to 0dp, layout_height is set to wrap_content,
+    // and layout_flexGrow is set to 1, the TextView is trying to expand to the vertical
+    // direction to enclose its content (in the measureHorizontal method), but
+    // the width will be expanded in this method. In that case, the height needs to be measured
+    // again with the expanded width.
+    var largestCrossSize = 0
+    if (!calledRecursively) {
+      flexLine.crossSize = Int.MIN_VALUE
+    }
+    var accumulatedRoundError = 0f
+    for (i in 0 until flexLine.itemCount) {
+      val index = flexLine.firstIndex + i
+      val child = getReorderedChildAt(index)
+      if (child == null || !child.visible) {
+        continue
+      }
+      if (flexDirection == FlexDirection.Row || flexDirection == FlexDirection.RowReverse) {
+        // The direction of the main axis is horizontal
+        var childMeasuredWidth = child.measuredWidth
+        if (measuredSizeCache != null) {
+          // Retrieve the measured width from the cache because there
+          // are some cases that the view is re-created from the last measure, thus
+          // View#getMeasuredWidth returns 0.
+          // E.g. if the flex container is FlexboxLayoutManager, the case happens
+          // frequently
+          childMeasuredWidth = unpackLower(measuredSizeCache!![index])
+        }
+        var childMeasuredHeight = child.measuredHeight
+        if (measuredSizeCache != null) {
+          // Extract the measured height from the cache
+          childMeasuredHeight = unpackHigher(measuredSizeCache!![index])
+        }
+        if (!childrenFrozen!![index] && child.flexGrow > 0f) {
+          var rawCalculatedWidth = (childMeasuredWidth + unitSpace * child.flexGrow)
+          if (i == flexLine.itemCount - 1) {
+            rawCalculatedWidth += accumulatedRoundError
+            accumulatedRoundError = 0f
+          }
+          var newWidth = rawCalculatedWidth.roundToInt()
+          if (newWidth > child.maxWidth) {
+            // This means the child can't expand beyond the value of the maxWidth
+            // attribute.
+            // To adjust the flex line length to the size of maxMainSize, remaining
+            // positive free space needs to be re-distributed to other flex items
+            // (children views). In that case, invoke this method again with the same
+            // fromIndex.
+            needsReexpand = true
+            newWidth = child.maxWidth
+            childrenFrozen!![index] = true
+            flexLine.totalFlexGrow -= child.flexGrow
+          } else {
+            accumulatedRoundError += rawCalculatedWidth - newWidth
+            if (accumulatedRoundError > 1.0) {
+              newWidth += 1
+              accumulatedRoundError -= 1.0f
+            } else if (accumulatedRoundError < -1.0) {
+              newWidth -= 1
+              accumulatedRoundError += 1.0f
+            }
+          }
+          val childHeightMeasureSpec = getChildHeightMeasureSpecInternal(
+            heightMeasureSpec,
+            child,
+            flexLine.sumCrossSizeBefore,
+          )
+          val childWidthMeasureSpec = MeasureSpec.from(newWidth, MeasureSpecMode.Exactly)
+          child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
+          childMeasuredWidth = child.measuredWidth
+          childMeasuredHeight = child.measuredHeight
+          updateMeasureCache(index, childWidthMeasureSpec, childHeightMeasureSpec, child)
+        }
+        largestCrossSize = max(
+          largestCrossSize,
+          childMeasuredHeight + child.margin.top + child.margin.bottom,
+        )
+        flexLine.mainSize += (childMeasuredWidth + child.margin.start + child.margin.end)
+      } else {
+        // The direction of the main axis is vertical
+        var childMeasuredHeight = child.measuredHeight
+        if (measuredSizeCache != null) {
+          // Retrieve the measured height from the cache because there
+          // are some cases that the view is re-created from the last measure, thus
+          // View#getMeasuredHeight returns 0.
+          // E.g. if the flex container is FlexboxLayoutManager, that case happens
+          // frequently
+          childMeasuredHeight = unpackHigher(measuredSizeCache!![index])
+        }
+        var childMeasuredWidth = child.measuredWidth
+        if (measuredSizeCache != null) {
+          // Extract the measured width from the cache
+          childMeasuredWidth = unpackLower(measuredSizeCache!![index])
+        }
+        if (!childrenFrozen!![index] && child.flexGrow > 0f) {
+          var rawCalculatedHeight = (childMeasuredHeight + unitSpace * child.flexGrow)
+          if (i == flexLine.itemCount - 1) {
+            rawCalculatedHeight += accumulatedRoundError
+            accumulatedRoundError = 0f
+          }
+          var newHeight = rawCalculatedHeight.roundToInt()
+          if (newHeight > child.maxHeight) {
+            // This means the child can't expand beyond the value of the maxHeight
+            // attribute.
+            // To adjust the flex line length to the size of maxMainSize, remaining
+            // positive free space needs to be re-distributed to other flex items
+            // (children views). In that case, invoke this method again with the same
+            // fromIndex.
+            needsReexpand = true
+            newHeight = child.maxHeight
+            childrenFrozen!![index] = true
+            flexLine.totalFlexGrow -= child.flexGrow
+          } else {
+            accumulatedRoundError += rawCalculatedHeight - newHeight
+            if (accumulatedRoundError > 1.0) {
+              newHeight += 1
+              accumulatedRoundError -= 1.0f
+            } else if (accumulatedRoundError < -1.0) {
+              newHeight -= 1
+              accumulatedRoundError += 1.0f
+            }
+          }
+          val childWidthMeasureSpec = getChildWidthMeasureSpecInternal(
+            widthMeasureSpec,
+            child,
+            flexLine.sumCrossSizeBefore,
+          )
+          val childHeightMeasureSpec = MeasureSpec.from(newHeight, MeasureSpecMode.Exactly)
+          child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
+          childMeasuredWidth = child.measuredWidth
+          childMeasuredHeight = child.measuredHeight
+          updateMeasureCache(index, childWidthMeasureSpec, childHeightMeasureSpec, child)
+        }
+        largestCrossSize = max(
+          largestCrossSize,
+          childMeasuredWidth + child.margin.start + child.margin.end,
+        )
+        flexLine.mainSize += (childMeasuredHeight + child.margin.top + child.margin.bottom)
+      }
+      flexLine.crossSize = max(flexLine.crossSize, largestCrossSize)
+    }
+    if (needsReexpand && sizeBeforeExpand != flexLine.mainSize) {
+      // Re-invoke the method with the same flex line to distribute the positive free space
+      // that wasn't fully distributed (because of maximum length constraint)
+      expandFlexItems(
+        widthMeasureSpec = widthMeasureSpec,
+        heightMeasureSpec = heightMeasureSpec,
+        flexLine = flexLine,
+        maxMainSize = maxMainSize,
+        paddingAlongMainAxis = paddingAlongMainAxis,
+        calledRecursively = true,
+      )
+    }
+  }
+
+  /**
+   * Shrink the flex items along the main axis based on the individual [Node.flexShrink] attribute.
+   *
+   * @param widthMeasureSpec the horizontal space requirements as imposed by the parent
+   * @param heightMeasureSpec the vertical space requirements as imposed by the parent
+   * @param flexLine the flex line to which flex items belong
+   * @param maxMainSize the maximum main size. Shrank main size will be this size
+   * @param paddingAlongMainAxis the padding value along the main axis
+   * @param calledRecursively true if this method is called recursively, false otherwise
+   */
+  private fun shrinkFlexItems(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+    flexLine: Line,
+    maxMainSize: Int,
+    paddingAlongMainAxis: Int,
+    calledRecursively: Boolean,
+  ) {
+    val sizeBeforeShrink = flexLine.mainSize
+    if (flexLine.totalFlexShrink <= 0 || maxMainSize > flexLine.mainSize) {
+      return
+    }
+    var needsReshrink = false
+    val unitShrink = (flexLine.mainSize - maxMainSize) / flexLine.totalFlexShrink
+    var accumulatedRoundError = 0f
+    flexLine.mainSize = paddingAlongMainAxis
+
+    // Setting the cross size of the flex line as the temporal value since the cross size of
+    // each flex item may be changed from the initial calculation
+    // (in the measureHorizontal/measureVertical method) even this method is part of the main
+    // size determination.
+    // E.g. If a TextView's layout_width is set to 0dp, layout_height is set to wrap_content,
+    // and layout_flexGrow is set to 1, the TextView is trying to expand to the vertical
+    // direction to enclose its content (in the measureHorizontal method), but
+    // the width will be expanded in this method. In that case, the height needs to be measured
+    // again with the expanded width.
+    var largestCrossSize = 0
+    if (!calledRecursively) {
+      flexLine.crossSize = Int.MIN_VALUE
+    }
+    for (i in 0 until flexLine.itemCount) {
+      val index = flexLine.firstIndex + i
+      val child = getReorderedChildAt(index)
+      if (child == null || !child.visible) {
+        continue
+      }
+      if (flexDirection == FlexDirection.Row || flexDirection == FlexDirection.RowReverse) {
+        // The direction of main axis is horizontal
+        var childMeasuredWidth = child.measuredWidth
+        if (measuredSizeCache != null) {
+          // Retrieve the measured width from the cache because there
+          // are some cases that the view is re-created from the last measure, thus
+          // View#getMeasuredWidth returns 0.
+          // E.g. if the flex container is FlexboxLayoutManager, the case happens
+          // frequently
+          childMeasuredWidth = unpackLower(measuredSizeCache!![index])
+        }
+        var childMeasuredHeight = child.measuredHeight
+        if (measuredSizeCache != null) {
+          // Extract the measured height from the cache
+          childMeasuredHeight = unpackHigher(measuredSizeCache!![index])
+        }
+        if (!childrenFrozen!![index] && child.flexShrink > 0f) {
+          var rawCalculatedWidth = (childMeasuredWidth - unitShrink * child.flexShrink)
+          if (i == flexLine.itemCount - 1) {
+            rawCalculatedWidth += accumulatedRoundError
+            accumulatedRoundError = 0f
+          }
+          var newWidth = rawCalculatedWidth.roundToInt()
+          if (newWidth < child.minWidth) {
+            // This means the child doesn't have enough space to distribute the negative
+            // free space. To adjust the flex line length down to the maxMainSize,
+            // remaining
+            // negative free space needs to be re-distributed to other flex items
+            // (children views). In that case, invoke this method again with the same
+            // fromIndex.
+            needsReshrink = true
+            newWidth = child.minWidth
+            childrenFrozen!![index] = true
+            flexLine.totalFlexShrink -= child.flexShrink
+          } else {
+            accumulatedRoundError += rawCalculatedWidth - newWidth
+            if (accumulatedRoundError > 1.0) {
+              newWidth += 1
+              accumulatedRoundError -= 1f
+            } else if (accumulatedRoundError < -1.0) {
+              newWidth -= 1
+              accumulatedRoundError += 1f
+            }
+          }
+          val childHeightMeasureSpec = getChildHeightMeasureSpecInternal(
+            heightMeasureSpec,
+            child,
+            flexLine.sumCrossSizeBefore,
+          )
+          val childWidthMeasureSpec = MeasureSpec.from(newWidth, MeasureSpecMode.Exactly)
+          child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
+          childMeasuredWidth = child.measuredWidth
+          childMeasuredHeight = child.measuredHeight
+          updateMeasureCache(index, childWidthMeasureSpec, childHeightMeasureSpec, child)
+        }
+        largestCrossSize = max(
+          largestCrossSize,
+          childMeasuredHeight + child.margin.top + child.margin.bottom,
+        )
+        flexLine.mainSize += (childMeasuredWidth + child.margin.start + child.margin.end)
+      } else {
+        // The direction of main axis is vertical
+        var childMeasuredHeight = child.measuredHeight
+        if (measuredSizeCache != null) {
+          // Retrieve the measured height from the cache because there
+          // are some cases that the view is re-created from the last measure, thus
+          // View#getMeasuredHeight returns 0.
+          // E.g. if the flex container is FlexboxLayoutManager, that case happens
+          // frequently
+          childMeasuredHeight = unpackHigher(measuredSizeCache!![index])
+        }
+        var childMeasuredWidth = child.measuredWidth
+        if (measuredSizeCache != null) {
+          // Extract the measured width from the cache
+          childMeasuredWidth = unpackLower(measuredSizeCache!![index])
+        }
+        if (!childrenFrozen!![index] && child.flexShrink > 0f) {
+          var rawCalculatedHeight = (
+            childMeasuredHeight -
+              unitShrink * child.flexShrink
+            )
+          if (i == flexLine.itemCount - 1) {
+            rawCalculatedHeight += accumulatedRoundError
+            accumulatedRoundError = 0f
+          }
+          var newHeight = rawCalculatedHeight.roundToInt()
+          if (newHeight < child.minHeight) {
+            // Need to invoke this method again like the case flex direction is vertical
+            needsReshrink = true
+            newHeight = child.minHeight
+            childrenFrozen!![index] = true
+            flexLine.totalFlexShrink -= child.flexShrink
+          } else {
+            accumulatedRoundError += rawCalculatedHeight - newHeight
+            if (accumulatedRoundError > 1.0) {
+              newHeight += 1
+              accumulatedRoundError -= 1f
+            } else if (accumulatedRoundError < -1.0) {
+              newHeight -= 1
+              accumulatedRoundError += 1f
+            }
+          }
+          val childWidthMeasureSpec = getChildWidthMeasureSpecInternal(
+            widthMeasureSpec,
+            child,
+            flexLine.sumCrossSizeBefore,
+          )
+          val childHeightMeasureSpec = MeasureSpec.from(newHeight, MeasureSpecMode.Exactly)
+          child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
+          childMeasuredWidth = child.measuredWidth
+          childMeasuredHeight = child.measuredHeight
+          updateMeasureCache(index, childWidthMeasureSpec, childHeightMeasureSpec, child)
+        }
+        largestCrossSize = max(
+          largestCrossSize,
+          childMeasuredWidth + child.margin.start + child.margin.end,
+        )
+        flexLine.mainSize += (childMeasuredHeight + child.margin.top + child.margin.bottom)
+      }
+      flexLine.crossSize = max(flexLine.crossSize, largestCrossSize)
+    }
+    if (needsReshrink && sizeBeforeShrink != flexLine.mainSize) {
+      // Re-invoke the method with the same fromIndex to distribute the negative free space
+      // that wasn't fully distributed (because some views length were not enough)
+      shrinkFlexItems(
+        widthMeasureSpec = widthMeasureSpec,
+        heightMeasureSpec = heightMeasureSpec,
+        flexLine = flexLine,
+        maxMainSize = maxMainSize,
+        paddingAlongMainAxis = paddingAlongMainAxis,
+        calledRecursively = true,
+      )
+    }
+  }
+
+  private fun getChildWidthMeasureSpecInternal(
+    widthMeasureSpec: MeasureSpec,
+    flexItem: Node,
+    padding: Int,
+  ): MeasureSpec {
+    var childWidthMeasureSpec = MeasureSpec.getChildMeasureSpec(
+      spec = widthMeasureSpec,
+      padding = this.padding.start + this.padding.end + flexItem.margin.start +
+        flexItem.margin.end + padding,
+      childDimension = flexItem.width,
+    )
+    val childWidth = childWidthMeasureSpec.size
+    if (childWidth > flexItem.maxWidth) {
+      childWidthMeasureSpec = MeasureSpec.from(flexItem.maxWidth, childWidthMeasureSpec.mode)
+    } else if (childWidth < flexItem.minWidth) {
+      childWidthMeasureSpec = MeasureSpec.from(flexItem.minWidth, childWidthMeasureSpec.mode)
+    }
+    return childWidthMeasureSpec
+  }
+
+  private fun getChildHeightMeasureSpecInternal(
+    heightMeasureSpec: MeasureSpec,
+    flexItem: Node,
+    padding: Int,
+  ): MeasureSpec {
+    var childHeightMeasureSpec = MeasureSpec.getChildMeasureSpec(
+      spec = heightMeasureSpec,
+      padding = this.padding.top + this.padding.bottom + flexItem.margin.top +
+        flexItem.margin.bottom + padding,
+      childDimension = flexItem.height,
+    )
+    val childHeight = childHeightMeasureSpec.size
+    if (childHeight > flexItem.maxHeight) {
+      childHeightMeasureSpec = MeasureSpec.from(flexItem.maxHeight, childHeightMeasureSpec.mode)
+    } else if (childHeight < flexItem.minHeight) {
+      childHeightMeasureSpec = MeasureSpec.from(flexItem.minHeight, childHeightMeasureSpec.mode)
+    }
+    return childHeightMeasureSpec
+  }
+
+  /**
+   * Determines the cross size (Calculate the length along the cross axis).
+   * Expand the cross size only if the height mode is [MeasureSpecMode.Exactly], otherwise
+   * use the sum of cross sizes of all flex lines.
+   *
+   * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+   * @param heightMeasureSpec vertical space requirements as imposed by the parent
+   * @param paddingAlongCrossAxis the padding value for the FlexboxLayout along the cross axis
+   */
+  internal fun determineCrossSize(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+    paddingAlongCrossAxis: Int,
+  ) {
+    val mode: MeasureSpecMode // The MeasureSpec mode along the cross axis
+    val size: Int // The MeasureSpec size along the cross axis
+    when (flexDirection) {
+      FlexDirection.Row, FlexDirection.RowReverse -> {
+        mode = heightMeasureSpec.mode
+        size = heightMeasureSpec.size
+      }
+      FlexDirection.Column, FlexDirection.ColumnReverse -> {
+        mode = widthMeasureSpec.mode
+        size = widthMeasureSpec.size
+      }
+      else -> error("Invalid FlexDirection: $flexDirection")
+    }
+    if (mode == MeasureSpecMode.Exactly) {
+      val totalCrossSize = getSumOfCrossSize() + paddingAlongCrossAxis
+      if (flexLines.size == 1) {
+        flexLines[0].crossSize = size - paddingAlongCrossAxis
+        // alignContent property is valid only if the Flexbox has at least two lines
+      } else if (flexLines.size >= 2) {
+        when (alignContent) {
+          AlignContent.Stretch -> run switch@{
+            if (totalCrossSize >= size) {
+              return@switch
+            }
+            val freeSpaceUnit = (size - totalCrossSize) / flexLines.size.toFloat()
+            var accumulatedError = 0f
+            var i = 0
+            val flexLinesSize = flexLines.size
+            while (i < flexLinesSize) {
+              val flexLine = flexLines[i]
+              var newCrossSizeAsFloat = flexLine.crossSize + freeSpaceUnit
+              if (i == flexLines.size - 1) {
+                newCrossSizeAsFloat += accumulatedError
+                accumulatedError = 0f
+              }
+              var newCrossSize = newCrossSizeAsFloat.roundToInt()
+              accumulatedError += newCrossSizeAsFloat - newCrossSize
+              if (accumulatedError > 1) {
+                newCrossSize += 1
+                accumulatedError -= 1f
+              } else if (accumulatedError < -1) {
+                newCrossSize -= 1
+                accumulatedError += 1f
+              }
+              flexLine.crossSize = newCrossSize
+              i++
+            }
+          }
+          AlignContent.SpaceAround -> run switch@{
+            if (totalCrossSize >= size) {
+              // If the size of the content is larger than the flex container, the
+              // Flex lines should be aligned center like ALIGN_CONTENT_CENTER
+              flexLines = constructFlexLinesForAlignContentCenter(
+                flexLines = flexLines,
+                size = size,
+                totalCrossSize = totalCrossSize,
+              )
+              return@switch
+            }
+            // The value of free space along the cross axis which needs to be put on top
+            // and below the bottom of each flex line.
+            var spaceTopAndBottom = size - totalCrossSize
+            // The number of spaces along the cross axis
+            val numberOfSpaces = flexLines.size * 2
+            spaceTopAndBottom /= numberOfSpaces
+            val newFlexLines = ArrayList<Line>()
+            val dummySpaceFlexLine = Line()
+            dummySpaceFlexLine.crossSize = spaceTopAndBottom
+            for (flexLine in flexLines) {
+              newFlexLines.add(dummySpaceFlexLine)
+              newFlexLines.add(flexLine)
+              newFlexLines.add(dummySpaceFlexLine)
+            }
+            flexLines = newFlexLines
+          }
+          AlignContent.SpaceBetween -> run switch@{
+            if (totalCrossSize >= size) {
+              return@switch
+            }
+            // The value of free space along the cross axis between each flex line.
+            var spaceBetweenFlexLine = (size - totalCrossSize).toFloat()
+            val numberOfSpaces = flexLines.size - 1
+            spaceBetweenFlexLine /= numberOfSpaces.toFloat()
+            var accumulatedError = 0f
+            val newFlexLines = ArrayList<Line>()
+            var i = 0
+            val flexLineSize = flexLines.size
+            while (i < flexLineSize) {
+              val flexLine = flexLines[i]
+              newFlexLines.add(flexLine)
+              if (i != flexLines.size - 1) {
+                val dummySpaceFlexLine = Line()
+                if (i == flexLines.size - 2) {
+                  // The last dummy space block in the flex container.
+                  // Adjust the cross size by the accumulated error.
+                  dummySpaceFlexLine.crossSize =
+                    (spaceBetweenFlexLine + accumulatedError).roundToInt()
+                  accumulatedError = 0f
+                } else {
+                  dummySpaceFlexLine.crossSize = spaceBetweenFlexLine.roundToInt()
+                }
+                accumulatedError += (spaceBetweenFlexLine - dummySpaceFlexLine.crossSize)
+                if (accumulatedError > 1) {
+                  dummySpaceFlexLine.crossSize += 1
+                  accumulatedError -= 1f
+                } else if (accumulatedError < -1) {
+                  dummySpaceFlexLine.crossSize -= 1
+                  accumulatedError += 1f
+                }
+                newFlexLines.add(dummySpaceFlexLine)
+              }
+              i++
+            }
+            flexLines = newFlexLines
+          }
+          AlignContent.Center -> {
+            flexLines = constructFlexLinesForAlignContentCenter(
+              flexLines = flexLines,
+              size = size,
+              totalCrossSize = totalCrossSize,
+            )
+          }
+          AlignContent.FlexEnd -> {
+            val spaceTop = size - totalCrossSize
+            val dummySpaceFlexLine = Line()
+            dummySpaceFlexLine.crossSize = spaceTop
+            flexLines = flexLines.toMutableList().apply { add(0, dummySpaceFlexLine) }
+          }
+        }
+      }
+    }
+  }
+
+  private fun constructFlexLinesForAlignContentCenter(
+    flexLines: List<Line>,
+    size: Int,
+    totalCrossSize: Int,
+  ): List<Line> {
+    var spaceAboveAndBottom = size - totalCrossSize
+    spaceAboveAndBottom /= 2
+    val newFlexLines = ArrayList<Line>()
+    val dummySpaceFlexLine = Line()
+    dummySpaceFlexLine.crossSize = spaceAboveAndBottom
+    var i = 0
+    val flexLineSize = flexLines.size
+    while (i < flexLineSize) {
+      if (i == 0) {
+        newFlexLines.add(dummySpaceFlexLine)
+      }
+      val flexLine = flexLines[i]
+      newFlexLines.add(flexLine)
+      if (i == flexLines.size - 1) {
+        newFlexLines.add(dummySpaceFlexLine)
+      }
+      i++
+    }
+    return newFlexLines
+  }
+
+  /**
+   * Expand the view if the [FlexboxEngine.alignItems] attribute is set to
+   * [AlignItems.Stretch] or [Node.alignSelf] is set as [AlignItems.Stretch].
+   *
+   * @param fromIndex the index from which value, stretch is calculated
+   */
+  internal fun stretchViews(fromIndex: Int = 0) {
+    if (fromIndex >= nodes.size) {
+      return
+    }
+    if (alignItems == AlignItems.Stretch) {
+      var flexLineIndex = 0
+      if (indexToFlexLine != null) {
+        flexLineIndex = indexToFlexLine!![fromIndex]
+      }
+      var i = flexLineIndex
+      val size = flexLines.size
+      while (i < size) {
+        val flexLine = flexLines[i]
+        var j = 0
+        val itemCount = flexLine.itemCount
+        while (j < itemCount) {
+          val viewIndex = flexLine.firstIndex + j
+          if (j >= nodes.size) {
+            j++
+            continue
+          }
+          val node = getReorderedChildAt(viewIndex)
+          if (node == null || !node.visible) {
+            j++
+            continue
+          }
+          if (node.alignSelf != AlignSelf.Auto && node.alignSelf != AlignSelf.Stretch) {
+            j++
+            continue
+          }
+          when (flexDirection) {
+            FlexDirection.Row, FlexDirection.RowReverse -> stretchViewVertically(
+              node = node,
+              crossSize = flexLine.crossSize,
+              index = viewIndex,
+            )
+            FlexDirection.Column, FlexDirection.ColumnReverse -> stretchViewHorizontally(
+              node = node,
+              crossSize = flexLine.crossSize,
+              index = viewIndex,
+            )
+            else -> error("Invalid FlexDirection: $flexDirection")
+          }
+          j++
+        }
+        i++
+      }
+    } else {
+      for (flexLine in flexLines) {
+        for (index in flexLine.indicesAlignSelfStretch) {
+          val view = getReorderedChildAt(index)!!
+          when (flexDirection) {
+            FlexDirection.Row, FlexDirection.RowReverse -> stretchViewVertically(
+              node = view,
+              crossSize = flexLine.crossSize,
+              index = index,
+            )
+            FlexDirection.Column, FlexDirection.ColumnReverse -> stretchViewHorizontally(
+              node = view,
+              crossSize = flexLine.crossSize,
+              index = index,
+            )
+            else -> throw IllegalArgumentException("Invalid flex direction: $flexDirection")
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Expand the view vertically to the size of the [crossSize] (considering the view margins)
+   *
+   * @param node the View to be stretched
+   * @param crossSize the cross size
+   * @param index the index of the view
+   */
+  private fun stretchViewVertically(node: Node, crossSize: Int, index: Int) {
+    var newHeight = crossSize - node.margin.top - node.margin.bottom
+    newHeight = max(newHeight, node.minHeight)
+    newHeight = min(newHeight, node.maxHeight)
+    val measuredWidth = if (measuredSizeCache != null) {
+      // Retrieve the measured height from the cache because there
+      // are some cases that the view is re-created from the last measure, thus
+      // View#getMeasuredHeight returns 0.
+      // E.g. if the flex container is FlexboxLayoutManager, that case happens
+      // frequently
+      unpackLower(measuredSizeCache!![index])
+    } else {
+      node.measuredWidth
+    }
+    val childWidthSpec = MeasureSpec.from(measuredWidth, MeasureSpecMode.Exactly)
+    val childHeightSpec = MeasureSpec.from(newHeight, MeasureSpecMode.Exactly)
+    node.measure(childWidthSpec, childHeightSpec)
+    updateMeasureCache(index, childWidthSpec, childHeightSpec, node)
+  }
+
+  /**
+   * Expand the view horizontally to the size of the crossSize (considering the view margins)
+   *
+   * @param node      the View to be stretched
+   * @param crossSize the cross size
+   * @param index     the index of the view
+   */
+  private fun stretchViewHorizontally(node: Node, crossSize: Int, index: Int) {
+    var newWidth = crossSize - node.margin.start - node.margin.end
+    newWidth = max(newWidth, node.minWidth)
+    newWidth = min(newWidth, node.maxWidth)
+    val measuredHeight = if (measuredSizeCache != null) {
+      // Retrieve the measured height from the cache because there
+      // are some cases that the view is re-created from the last measure, thus
+      // View#getMeasuredHeight returns 0.
+      // E.g. if the flex container is FlexboxLayoutManager, that case happens
+      // frequently
+      unpackHigher(measuredSizeCache!![index])
+    } else {
+      node.measuredHeight
+    }
+    val childHeightSpec = MeasureSpec.from(measuredHeight, MeasureSpecMode.Exactly)
+    val childWidthSpec = MeasureSpec.from(newWidth, MeasureSpecMode.Exactly)
+    node.measure(childWidthSpec, childHeightSpec)
+    updateMeasureCache(index, childWidthSpec, childHeightSpec, node)
+  }
+
+  /**
+   * Place a single View when the layout direction is horizontal
+   * ([FlexboxEngine.flexDirection] is either [FlexDirection.Row] or
+   * [FlexDirection.RowReverse]).
+   *
+   * @param node the View to be placed
+   * @param flexLine the [Line] where the View belongs to
+   * @param left the left position of the View, which the View's margin is already taken
+   * into account
+   * @param top the top position of the flex line where the View belongs to. The actual
+   * View's top position is shifted depending on the flexWrap and alignItems attributes
+   * @param right the right position of the View, which the View's margin is already taken
+   * into account
+   * @param bottom the bottom position of the flex line where the View belongs to. The actual
+   * View's bottom position is shifted depending on the flexWrap and alignItems attributes
+   * @see Node.alignSelf
+   */
+  private fun layoutSingleChildHorizontal(
+    node: Node,
+    flexLine: Line,
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+  ) {
+    var alignItems = alignItems
+    if (node.alignSelf != AlignSelf.Auto) {
+      // Expecting the values for alignItems and alignSelf match except for ALIGN_SELF_AUTO.
+      // Assigning the alignSelf value as alignItems should work.
+      alignItems = AlignItems(node.alignSelf.ordinal)
+    }
+    val crossSize = flexLine.crossSize
+    when (alignItems) {
+      AlignItems.FlexStart, AlignItems.Stretch -> if (flexWrap != FlexWrap.WrapReverse) {
+        node.layout(left, top + node.margin.top, right, bottom + node.margin.top)
+      } else {
+        node.layout(left, top - node.margin.bottom, right, bottom - node.margin.bottom)
+      }
+      AlignItems.Baseline -> if (flexWrap != FlexWrap.WrapReverse) {
+        val marginTop = max(flexLine.maxBaseline - node.baseline, node.margin.top)
+        node.layout(left, top + marginTop, right, bottom + marginTop)
+      } else {
+        val marginBottom = max(flexLine.maxBaseline - node.measuredHeight + node.baseline, node.margin.bottom)
+        node.layout(left, top - marginBottom, right, bottom - marginBottom)
+      }
+      AlignItems.FlexEnd -> if (flexWrap != FlexWrap.WrapReverse) {
+        node.layout(
+          left = left,
+          top = top + crossSize - node.measuredHeight - node.margin.bottom,
+          right = right,
+          bottom = top + crossSize - node.margin.bottom,
+        )
+      } else {
+        // If the flexWrap == WRAP_REVERSE, the direction of the
+        // flexEnd is flipped (from top to bottom).
+        node.layout(
+          left = left,
+          top = top - crossSize + node.measuredHeight + node.margin.top,
+          right = right,
+          bottom = bottom - crossSize + node.measuredHeight + node.margin.top,
+        )
+      }
+      AlignItems.Center -> {
+        val topFromCrossAxis = (crossSize - node.measuredHeight + node.margin.top - node.margin.bottom) / 2
+        if (flexWrap != FlexWrap.WrapReverse) {
+          node.layout(
+            left = left,
+            top = top + topFromCrossAxis,
+            right = right,
+            bottom = top + topFromCrossAxis + node.measuredHeight,
+          )
+        } else {
+          node.layout(
+            left = left,
+            top = top - topFromCrossAxis,
+            right = right,
+            bottom = top - topFromCrossAxis + node.measuredHeight,
+          )
+        }
+      }
+    }
+  }
+
+  /**
+   * Place a single View when the layout direction is vertical
+   * ([FlexboxEngine.flexDirection] is either [FlexDirection.Column] or
+   * [FlexDirection.ColumnReverse]).
+   *
+   * @param node     the View to be placed
+   * @param flexLine the [Line] where the View belongs to
+   * @param isRtl    `true` if the layout direction is right to left, `false`
+   * otherwise
+   * @param left     the left position of the flex line where the View belongs to. The actual
+   * View's left position is shifted depending on the isLayoutRtl and alignItems
+   * attributes
+   * @param top      the top position of the View, which the View's margin is already taken
+   * into account
+   * @param right    the right position of the flex line where the View belongs to. The actual
+   * View's right position is shifted depending on the isLayoutRtl and alignItems
+   * attributes
+   * @param bottom   the bottom position of the View, which the View's margin is already taken
+   * into account
+   * @see Node.alignSelf
+   */
+  private fun layoutSingleChildVertical(
+    node: Node,
+    flexLine: Line,
+    isRtl: Boolean,
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+  ) {
+    var alignItems = alignItems
+    if (node.alignSelf != AlignSelf.Auto) {
+      // Expecting the values for alignItems and alignSelf match except for ALIGN_SELF_AUTO.
+      // Assigning the alignSelf value as alignItems should work.
+      alignItems = AlignItems(node.alignSelf.ordinal)
+    }
+    val crossSize = flexLine.crossSize
+    when (alignItems) {
+      AlignItems.FlexStart, AlignItems.Stretch, AlignItems.Baseline -> if (!isRtl) {
+        node.layout(
+          left = left + node.margin.start,
+          top = top,
+          right = right + node.margin.start,
+          bottom = bottom,
+        )
+      } else {
+        node.layout(
+          left = left - node.margin.end,
+          top = top,
+          right = right - node.margin.end,
+          bottom = bottom,
+        )
+      }
+      AlignItems.FlexEnd -> if (!isRtl) {
+        node.layout(
+          left = left + crossSize - node.measuredWidth - node.margin.end,
+          top = top,
+          right = right + crossSize - node.measuredWidth - node.margin.end,
+          bottom = bottom,
+        )
+      } else {
+        // If the flexWrap == WRAP_REVERSE, the direction of the
+        // flexEnd is flipped (from left to right).
+        node.layout(
+          left = left - crossSize + node.measuredWidth + node.margin.start,
+          top = top,
+          right = right - crossSize + node.measuredWidth + node.margin.start,
+          bottom = bottom,
+        )
+      }
+      AlignItems.Center -> {
+        val leftFromCrossAxis = (crossSize - node.measuredWidth + node.margin.start - node.margin.end) / 2
+        if (!isRtl) {
+          node.layout(left + leftFromCrossAxis, top, right + leftFromCrossAxis, bottom)
+        } else {
+          node.layout(left - leftFromCrossAxis, top, right - leftFromCrossAxis, bottom)
+        }
+      }
+    }
+  }
+
+  private fun updateMeasureCache(
+    index: Int,
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+    node: Node,
+  ) {
+    if (measureSpecCache != null) {
+      measureSpecCache!![index] = packLong(widthMeasureSpec.value, heightMeasureSpec.value)
+    }
+    if (measuredSizeCache != null) {
+      measuredSizeCache!![index] = packLong(node.measuredWidth, node.measuredHeight)
+    }
+  }
+
+  internal fun ensureIndexToFlexLine(size: Int) {
+    if (indexToFlexLine == null) {
+      indexToFlexLine = IntArray(max(size, 10))
+    } else if (indexToFlexLine!!.size < size) {
+      val newCapacity = max(indexToFlexLine!!.size * 2, size)
+      indexToFlexLine = indexToFlexLine!!.copyOf(newCapacity)
+    }
+  }
+
+  public fun measure(widthSpec: MeasureSpec, heightSpec: MeasureSpec): Size {
+    return when (flexDirection) {
+      FlexDirection.Row, FlexDirection.RowReverse -> measureHorizontal(widthSpec, heightSpec)
+      FlexDirection.Column, FlexDirection.ColumnReverse -> measureVertical(widthSpec, heightSpec)
+      else -> error("Invalid FlexDirection: $flexDirection")
+    }
+  }
+
+  /**
+   * Sub method for `onMeasure`, when the main axis direction is horizontal
+   * (either left to right or right to left).
+   *
+   * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+   * @param heightMeasureSpec vertical space requirements as imposed by the parent
+   */
+  private fun measureHorizontal(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+  ): Size {
+    val flexLines = calculateHorizontalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    determineMainSize(widthMeasureSpec, heightMeasureSpec)
+
+    if (alignItems == AlignItems.Baseline) {
+      for (flexLine in flexLines) {
+        // The largest height value that also take the baseline shift into account
+        var largestHeightInLine = Int.MIN_VALUE
+        for (i in 0 until flexLine.itemCount) {
+          val viewIndex = flexLine.firstIndex + i
+          val child = getReorderedChildAt(viewIndex)
+          if (child == null || !child.visible) {
+            continue
+          }
+          largestHeightInLine = if (flexWrap != FlexWrap.WrapReverse) {
+            val marginTop = max(flexLine.maxBaseline - child.baseline, child.margin.top)
+            child.measuredHeight + marginTop + child.margin.bottom
+          } else {
+            val marginBottom = max(
+              flexLine.maxBaseline - child.measuredHeight +
+                child.baseline,
+              child.margin.bottom,
+            )
+            child.measuredHeight + child.margin.top + marginBottom
+          }.coerceAtLeast(largestHeightInLine)
+        }
+        flexLine.crossSize = largestHeightInLine
+      }
+    }
+    determineCrossSize(
+      widthMeasureSpec = widthMeasureSpec,
+      heightMeasureSpec = heightMeasureSpec,
+      paddingAlongCrossAxis = padding.top + padding.bottom,
+    )
+    // Now cross size for each flex line is determined.
+    // Expand the views if alignItems (or alignSelf in each child view) is set to stretch
+    stretchViews()
+    return setMeasuredDimensionForFlex(
+      flexDirection = flexDirection,
+      widthMeasureSpec = widthMeasureSpec,
+      heightMeasureSpec = heightMeasureSpec,
+    )
+  }
+
+  /**
+   * Sub method for `onMeasure` when the main axis direction is vertical
+   * (either from top to bottom or bottom to top).
+   *
+   * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+   * @param heightMeasureSpec vertical space requirements as imposed by the parent
+   */
+  private fun measureVertical(
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+  ): Size {
+    calculateVerticalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    determineMainSize(widthMeasureSpec, heightMeasureSpec)
+    determineCrossSize(
+      widthMeasureSpec = widthMeasureSpec,
+      heightMeasureSpec = heightMeasureSpec,
+      paddingAlongCrossAxis = padding.start + padding.end,
+    )
+    // Now cross size for each flex line is determined.
+    // Expand the views if alignItems (or alignSelf in each child view) is set to stretch
+    stretchViews()
+    return setMeasuredDimensionForFlex(
+      flexDirection = flexDirection,
+      widthMeasureSpec = widthMeasureSpec,
+      heightMeasureSpec = heightMeasureSpec,
+    )
+  }
+
+  /**
+   * Set this FlexboxLayouts' width and height depending on the calculated size of main axis and
+   * cross axis.
+   *
+   * @param flexDirection the value of the flex direction
+   * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+   * @param heightMeasureSpec vertical space requirements as imposed by the parent
+   */
+  private fun setMeasuredDimensionForFlex(
+    flexDirection: FlexDirection,
+    widthMeasureSpec: MeasureSpec,
+    heightMeasureSpec: MeasureSpec,
+  ): Size {
+    val calculatedMaxHeight: Int
+    val calculatedMaxWidth: Int
+    when (flexDirection) {
+      FlexDirection.Row, FlexDirection.RowReverse -> {
+        calculatedMaxHeight = getSumOfCrossSize() + padding.top + padding.bottom
+        calculatedMaxWidth = getLargestMainSize()
+      }
+      FlexDirection.Column, FlexDirection.ColumnReverse -> {
+        calculatedMaxHeight = getLargestMainSize()
+        calculatedMaxWidth = getSumOfCrossSize() + padding.start + padding.end
+      }
+      else -> error("Invalid FlexDirection: $flexDirection")
+    }
+    val widthSize = widthMeasureSpec.size
+    val width = when (val widthMode = widthMeasureSpec.mode) {
+      MeasureSpecMode.Exactly -> {
+        MeasureSpec.resolveSize(widthSize, widthMeasureSpec)
+      }
+      MeasureSpecMode.AtMost -> {
+        MeasureSpec.resolveSize(min(widthSize, calculatedMaxWidth), widthMeasureSpec)
+      }
+      MeasureSpecMode.Unspecified -> {
+        MeasureSpec.resolveSize(calculatedMaxWidth, widthMeasureSpec)
+      }
+      else -> error("Unknown width mode: $widthMode")
+    }
+    val heightSize = heightMeasureSpec.size
+    val height = when (val heightMode = heightMeasureSpec.mode) {
+      MeasureSpecMode.Exactly -> {
+        MeasureSpec.resolveSize(heightSize, heightMeasureSpec)
+      }
+      MeasureSpecMode.AtMost -> {
+        MeasureSpec.resolveSize(min(heightSize, calculatedMaxHeight), heightMeasureSpec)
+      }
+      MeasureSpecMode.Unspecified -> {
+        MeasureSpec.resolveSize(calculatedMaxHeight, heightMeasureSpec)
+      }
+      else -> error("Unknown height mode: $heightMode")
+    }
+    return Size(width, height)
+  }
+
+  public fun layout(left: Int, top: Int, right: Int, bottom: Int) {
+    when (flexDirection) {
+      FlexDirection.Row -> {
+        layoutHorizontal(false, left, top, right, bottom)
+      }
+      FlexDirection.RowReverse -> {
+        layoutHorizontal(true, left, top, right, bottom)
+      }
+      FlexDirection.Column -> {
+        val isRtl = flexWrap == FlexWrap.WrapReverse
+        layoutVertical(isRtl, false, left, top, right, bottom)
+      }
+      FlexDirection.ColumnReverse -> {
+        val isRtl = flexWrap != FlexWrap.WrapReverse
+        layoutVertical(isRtl, true, left, top, right, bottom)
+      }
+      else -> error("Invalid FlexDirection: $flexDirection")
+    }
+  }
+
+  /**
+   * Sub method for `onLayout` when the [FlexboxEngine.flexDirection] is either
+   * [FlexDirection.Row] or [FlexDirection.RowReverse].
+   *
+   * @param isRtl `true` if the horizontal layout direction is right to left, `false` otherwise.
+   * @param left the left position of this View
+   * @param top the top position of this View
+   * @param right the right position of this View
+   * @param bottom the bottom position of this View
+   */
+  private fun layoutHorizontal(
+    isRtl: Boolean,
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+  ) {
+    val paddingLeft = padding.start
+    val paddingRight = padding.end
+    // Use float to reduce the round error that may happen in when justifyContent ==
+    // SPACE_BETWEEN or SPACE_AROUND
+    var childLeft: Float
+    val height = bottom - top
+    val width = right - left
+    // childBottom is used if the flexWrap is WRAP_REVERSE otherwise
+    // childTop is used to align the vertical position of the children views.
+    var childBottom = height - padding.bottom
+    var childTop = padding.top
+
+    // Used only for RTL layout
+    // Use float to reduce the round error that may happen in when justifyContent ==
+    // SPACE_BETWEEN or SPACE_AROUND
+    var childRight: Float
+    var i = 0
+    val size = flexLines.size
+    while (i < size) {
+      val flexLine = flexLines[i]
+      var spaceBetweenItem = 0f
+      when (justifyContent) {
+        JustifyContent.FlexStart -> {
+          childLeft = paddingLeft.toFloat()
+          childRight = (width - paddingRight).toFloat()
+        }
+        JustifyContent.FlexEnd -> {
+          childLeft = (width - flexLine.mainSize + paddingRight).toFloat()
+          childRight = (flexLine.mainSize - paddingLeft).toFloat()
+        }
+        JustifyContent.Center -> {
+          childLeft = paddingLeft + (width - flexLine.mainSize) / 2f
+          childRight = width - paddingRight - (width - flexLine.mainSize) / 2f
+        }
+        JustifyContent.SpaceAround -> {
+          val visibleCount = flexLine.itemCountVisible
+          if (visibleCount != 0) {
+            spaceBetweenItem = ((width - flexLine.mainSize) / visibleCount.toFloat())
+          }
+          childLeft = paddingLeft + spaceBetweenItem / 2f
+          childRight = width - paddingRight - spaceBetweenItem / 2f
+        }
+        JustifyContent.SpaceBetween -> {
+          childLeft = paddingLeft.toFloat()
+          val visibleCount = flexLine.itemCountVisible
+          val denominator = if (visibleCount != 1) (visibleCount - 1).toFloat() else 1f
+          spaceBetweenItem = (width - flexLine.mainSize) / denominator
+          childRight = (width - paddingRight).toFloat()
+        }
+        JustifyContent.SpaceEvenly -> {
+          val visibleCount = flexLine.itemCountVisible
+          if (visibleCount != 0) {
+            spaceBetweenItem = ((width - flexLine.mainSize) / (visibleCount + 1).toFloat())
+          }
+          childLeft = paddingLeft + spaceBetweenItem
+          childRight = width - paddingRight - spaceBetweenItem
+        }
+        else -> error("Invalid JustifyContent: $justifyContent")
+      }
+      spaceBetweenItem = max(spaceBetweenItem, 0f)
+      for (j in 0 until flexLine.itemCount) {
+        val index = flexLine.firstIndex + j
+        val child = getReorderedChildAt(index)
+        if (child == null || !child.visible) {
+          continue
+        }
+        childLeft += child.margin.start.toFloat()
+        childRight -= child.margin.end.toFloat()
+        if (flexWrap == FlexWrap.WrapReverse) {
+          if (isRtl) {
+            layoutSingleChildHorizontal(
+              node = child,
+              flexLine = flexLine,
+              left = childRight.roundToInt() - child.measuredWidth,
+              top = childBottom - child.measuredHeight,
+              right = childRight.roundToInt(),
+              bottom = childBottom,
+            )
+          } else {
+            layoutSingleChildHorizontal(
+              node = child,
+              flexLine = flexLine,
+              left = childLeft.roundToInt(),
+              top = childBottom - child.measuredHeight,
+              right = childLeft.roundToInt() + child.measuredWidth,
+              bottom = childBottom,
+            )
+          }
+        } else {
+          if (isRtl) {
+            layoutSingleChildHorizontal(
+              node = child,
+              flexLine = flexLine,
+              left = childRight.roundToInt() - child.measuredWidth,
+              top = childTop,
+              right = childRight.roundToInt(),
+              bottom = childTop + child.measuredHeight,
+            )
+          } else {
+            layoutSingleChildHorizontal(
+              node = child,
+              flexLine = flexLine,
+              left = childLeft.roundToInt(),
+              top = childTop,
+              right = childLeft.roundToInt() + child.measuredWidth,
+              bottom = childTop + child.measuredHeight,
+            )
+          }
+        }
+        childLeft += child.measuredWidth + spaceBetweenItem + child.margin.end
+        childRight -= child.measuredWidth + spaceBetweenItem + child.margin.start
+      }
+      childTop += flexLine.crossSize
+      childBottom -= flexLine.crossSize
+      i++
+    }
+  }
+
+  /**
+   * Sub method for `onLayout` when the [FlexboxEngine.flexDirection] is either
+   * [FlexDirection.Column] or [FlexDirection.ColumnReverse].
+   *
+   * @param isRtl `true` if the horizontal layout direction is right to left, `false` otherwise
+   * @param fromBottomToTop `true` if the layout direction is bottom to top, `false` otherwise
+   * @param left the left position of this View
+   * @param top the top position of this View
+   * @param right the right position of this View
+   * @param bottom the bottom position of this View
+   */
+  private fun layoutVertical(
+    isRtl: Boolean,
+    fromBottomToTop: Boolean,
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+  ) {
+    val paddingTop = padding.top
+    val paddingBottom = padding.bottom
+    val paddingRight = padding.end
+    var childLeft = padding.start
+    val width = right - left
+    val height = bottom - top
+    // childRight is used if the flexWrap is WRAP_REVERSE otherwise
+    // childLeft is used to align the horizontal position of the children views.
+    var childRight = width - paddingRight
+
+    // Use float to reduce the round error that may happen in when justifyContent ==
+    // SPACE_BETWEEN or SPACE_AROUND
+    var childTop: Float
+
+    // Used only for if the direction is from bottom to top
+    var childBottom: Float
+    var i = 0
+    val size = flexLines.size
+    while (i < size) {
+      val flexLine = flexLines[i]
+      var spaceBetweenItem = 0f
+      when (justifyContent) {
+        JustifyContent.FlexStart -> {
+          childTop = paddingTop.toFloat()
+          childBottom = (height - paddingBottom).toFloat()
+        }
+        JustifyContent.FlexEnd -> {
+          childTop = (height - flexLine.mainSize + paddingBottom).toFloat()
+          childBottom = (flexLine.mainSize - paddingTop).toFloat()
+        }
+        JustifyContent.Center -> {
+          childTop = paddingTop + (height - flexLine.mainSize) / 2f
+          childBottom = height - paddingBottom - (height - flexLine.mainSize) / 2f
+        }
+        JustifyContent.SpaceAround -> {
+          val visibleCount = flexLine.itemCountVisible
+          if (visibleCount != 0) {
+            spaceBetweenItem = ((height - flexLine.mainSize) / visibleCount.toFloat())
+          }
+          childTop = paddingTop + spaceBetweenItem / 2f
+          childBottom = height - paddingBottom - spaceBetweenItem / 2f
+        }
+        JustifyContent.SpaceBetween -> {
+          childTop = paddingTop.toFloat()
+          val visibleCount = flexLine.itemCountVisible
+          val denominator = if (visibleCount != 1) (visibleCount - 1).toFloat() else 1f
+          spaceBetweenItem = (height - flexLine.mainSize) / denominator
+          childBottom = (height - paddingBottom).toFloat()
+        }
+        JustifyContent.SpaceEvenly -> {
+          val visibleCount = flexLine.itemCountVisible
+          if (visibleCount != 0) {
+            spaceBetweenItem = ((height - flexLine.mainSize) / (visibleCount + 1).toFloat())
+          }
+          childTop = paddingTop + spaceBetweenItem
+          childBottom = height - paddingBottom - spaceBetweenItem
+        }
+        else -> error("Invalid JustifyContent: $justifyContent")
+      }
+      spaceBetweenItem = max(spaceBetweenItem, 0f)
+      for (j in 0 until flexLine.itemCount) {
+        val index = flexLine.firstIndex + j
+        val child = getReorderedChildAt(index)
+        if (child == null || !child.visible) {
+          continue
+        }
+        childTop += child.margin.top.toFloat()
+        childBottom -= child.margin.bottom.toFloat()
+        if (isRtl) {
+          if (fromBottomToTop) {
+            layoutSingleChildVertical(
+              node = child,
+              flexLine = flexLine,
+              isRtl = true,
+              left = childRight - child.measuredWidth,
+              top = childBottom.roundToInt() - child.measuredHeight,
+              right = childRight,
+              bottom = childBottom.roundToInt(),
+            )
+          } else {
+            layoutSingleChildVertical(
+              node = child,
+              flexLine = flexLine,
+              isRtl = true,
+              left = childRight - child.measuredWidth,
+              top = childTop.roundToInt(),
+              right = childRight,
+              bottom = childTop.roundToInt() + child.measuredHeight,
+            )
+          }
+        } else {
+          if (fromBottomToTop) {
+            layoutSingleChildVertical(
+              node = child,
+              flexLine = flexLine,
+              isRtl = false,
+              left = childLeft,
+              top = childBottom.roundToInt() - child.measuredHeight,
+              right = childLeft + child.measuredWidth,
+              bottom = childBottom.roundToInt(),
+            )
+          } else {
+            layoutSingleChildVertical(
+              node = child,
+              flexLine = flexLine,
+              isRtl = false,
+              left = childLeft,
+              top = childTop.roundToInt(),
+              right = childLeft + child.measuredWidth,
+              bottom = childTop.roundToInt() + child.measuredHeight,
+            )
+          }
+        }
+        childTop += child.measuredHeight + spaceBetweenItem + child.margin.bottom
+        childBottom -= child.measuredHeight + spaceBetweenItem + child.margin.top
+      }
+      childLeft += flexLine.crossSize
+      childRight -= flexLine.crossSize
+      i++
+    }
+  }
+
+  /**
+   * The largest main size of all flex lines including decorator lengths.
+   */
+  private fun getLargestMainSize(): Int {
+    return if (flexLines.isEmpty()) Int.MIN_VALUE else flexLines.maxOf { it.mainSize }
+  }
+
+  /**
+   * The sum of the cross sizes of all flex lines including decorator lengths.
+   */
+  private fun getSumOfCrossSize(): Int {
+    return flexLines.sumOf { it.crossSize }
+  }
+
+  /**
+   * Returns a View, which is reordered by taking [Node.order] parameters into account.
+   *
+   * @param index the index of the view
+   * @return the reordered view, which [Node.order] is taken into account.
+   * If the index is negative or out of bounds of the number of contained views, returns `null`.
+   */
+  private fun getReorderedChildAt(index: Int): Node? {
+    if (indexToReorderedIndex == null) {
+      val sorted = nodes.withIndex().sortedWith(compareBy({ -it.value.order }, { it.index }))
+      val indexes = IntArray(sorted.size)
+      sorted.forEachIndexed { i, value ->
+        indexes[value.index] = i
+      }
+      indexToReorderedIndex = indexes
+    }
+    return indexToReorderedIndex?.getOrNull(index)?.let(nodes::getOrNull)
+  }
+}
diff --git a/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/Line.kt b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/Line.kt
new file mode 100644
index 000000000..c93bfb52a
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/Line.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood
+
+import kotlin.jvm.JvmField
+
+/**
+ * Holds properties related to a single flex line.
+ */
+internal class Line {
+
+  /**
+   * The size of the flex line in pixels along the main axis of the flex container.
+   */
+  @JvmField var mainSize = 0
+
+  /**
+   * The size of the flex line in pixels along the cross axis of the flex container.
+   */
+  @JvmField var crossSize = 0
+
+  /**
+   * The count of the views contained in this flex line.
+   */
+  @JvmField var itemCount = 0
+
+  /**
+   * Holds the count of the views whose visibilities are gone.
+   */
+  @JvmField var invisibleItemCount = 0
+
+  /**
+   * The sum of the flexGrow properties of the children included in this flex line.
+   */
+  @JvmField var totalFlexGrow = 0f
+
+  /**
+   * The sum of the flexShrink properties of the children included in this flex line.
+   */
+  @JvmField var totalFlexShrink = 0f
+
+  /**
+   * The largest value of the individual child's baseline.
+   */
+  @JvmField var maxBaseline = 0
+
+  /**
+   * The sum of the cross size used before this flex line.
+   */
+  @JvmField var sumCrossSizeBefore = 0
+
+  /**
+   * Store the indices of the children views whose alignSelf property is stretch.
+   * The stored indices are the absolute indices including all children in the Flexbox,
+   * not the relative indices in this flex line.
+   */
+  @JvmField var indicesAlignSelfStretch = listOf<Int>()
+
+  /**
+   * The first view's index included in this flex line.
+   */
+  @JvmField var firstIndex = 0
+
+  /**
+   * The last view's index included in this flex line.
+   */
+  @JvmField var lastIndex = 0
+
+  /**
+   * Set to true if any [Node]s in this line have [Node.flexGrow] attributes set
+   * (i.e. have a value other than [Node.DefaultFlexGrow])
+   */
+  @JvmField var anyItemsHaveFlexGrow = false
+
+  /**
+   * Set to true if any [Node]s in this line have [Node.flexShrink] attributes set
+   * (i.e. have a value other than [Node.UndefinedFlexShrink])
+   */
+  @JvmField var anyItemsHaveFlexShrink = false
+}
diff --git a/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/Node.kt b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/Node.kt
new file mode 100644
index 000000000..2716e84ab
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/Node.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood
+
+/**
+ * An interface that has the common behavior as a node contained in a flexbox.
+ */
+public interface Node {
+
+  /**
+   * The width attribute of the node.
+   *
+   * The attribute is about how wide the view wants to be. Can be one of the
+   * constants [MatchParent] or [WrapContent], or an exact size.
+   */
+  public val width: Int
+
+  /**
+   * The height attribute of the node.
+   *
+   * The attribute is about how wide the view wants to be. Can be one of the
+   * constants [MatchParent] or [WrapContent], or an exact size.
+   */
+  public val height: Int
+
+  /**
+   * The minimum width attribute of the node.
+   *
+   * The attribute determines the minimum width the child can shrink to.
+   */
+  public val minWidth: Int
+
+  /**
+   * The minimum height attribute of the node.
+   *
+   * The attribute determines the minimum height the child can shrink to.
+   */
+  public val minHeight: Int
+
+  /**
+   * The maximum width attribute of the node.
+   *
+   * The attribute determines the maximum width the child can expand to.
+   */
+  public val maxWidth: Int
+
+  /**
+   * The maximum height attribute of the node.
+   */
+  public val maxHeight: Int
+
+  /**
+   * True if this item is visible and should be laid out.
+   */
+  public val visible: Boolean
+
+  /**
+   * The baseline used for [AlignItems.Baseline] and [AlignSelf.Baseline].
+   * If not specified, -1 is set as a default value.
+   */
+  public val baseline: Int
+
+  /**
+   * The order attribute of the node.
+   *
+   * The attribute can change the order in which the children are laid out.
+   * By default, children are displayed and laid out in the same order as they added to the
+   * [FlexboxEngine]. If not specified, [DefaultOrder] is set as a default value.
+   */
+  public val order: Int
+
+  /**
+   * The flex grow attribute of the node.
+   *
+   * The attribute determines how much this child will grow if positive free space is
+   * distributed relative to the rest of other nodes included in the same flex line.
+   * If not specified, [DefaultFlexGrow] is set as a default value.
+   */
+  public val flexGrow: Float
+
+  /**
+   * The flex shrink attribute of the node.
+   *
+   * The attribute determines how much this child will shrink if negative free space is
+   * distributed relative to the rest of other nodes included in the same flex line.
+   * If not specified, [DefaultFlexShrink] is set as a default value.
+   */
+  public val flexShrink: Float
+
+  /**
+   * The flexBasisPercent attribute of the node.
+   *
+   * The attribute determines the initial node length in a fraction format relative to its
+   * parent.
+   * The initial main size of this child View is trying to be expanded as the specified
+   * fraction against the parent main size.
+   * If this value is set, the length specified from layout_width
+   * (or layout_height) is overridden by the calculated value from this attribute.
+   * This attribute is only effective when the parent's MeasureSpec mode is
+   * MeasureSpec.EXACTLY. The default value is -1, which means not set.
+   */
+  public val flexBasisPercent: Float
+
+  /**
+   * The align self attribute of the node.
+   *
+   * The attribute determines the alignment along the cross axis (perpendicular to the
+   * main axis). The alignment in the same direction can be determined by the
+   * align items attribute in the parent, but if this is set to other than
+   * [AlignSelf.Auto], the cross axis alignment is overridden for this child.
+   * The value needs to be one of the values in ([AlignSelf.Auto],
+   * [AlignItems.Stretch], [AlignItems.FlexStart], [AlignItems.FlexEnd],
+   * [AlignItems.Center], or [AlignItems.Baseline]).
+   * If not specified, [AlignSelf.Auto] is set as a default value.
+   */
+  public val alignSelf: AlignSelf
+
+  /**
+   * The wrapBefore attribute of the node.
+   *
+   * The attribute forces a flex line wrapping. i.e. if this is set to `true` for a
+   * node, the item will become the first item of the new flex line. (A wrapping happens
+   * regardless of the nodes being processed in the the previous flex line)
+   * This attribute is ignored if the flex_wrap attribute is set as nowrap.
+   * The equivalent attribute isn't defined in the original CSS Flexible Box Module
+   * specification, but having this attribute is useful for Android developers to flatten
+   * the layouts when building a grid like layout or for a situation where developers want
+   * to put a new flex line to make a semantic difference from the previous one, etc.
+   */
+  public val wrapBefore: Boolean
+
+  /**
+   * The margin of the node.
+   */
+  public val margin: Spacing
+
+  /**
+   * The measured width after invoking [measure].
+   */
+  public val measuredWidth: Int
+
+  /**
+   * The measured height after invoking [measure].
+   */
+  public val measuredHeight: Int
+
+  /**
+   * Measure this node according to the [widthSpec] and [heightSpec] constraints and update
+   * [measuredWidth] and [measuredHeight] with the result.
+   */
+  public fun measure(widthSpec: MeasureSpec, heightSpec: MeasureSpec)
+
+  /**
+   * Place the node inside the given bounding box.
+   */
+  public fun layout(left: Int, top: Int, right: Int, bottom: Int)
+
+  public companion object {
+    /**
+     * A special constant for [width] or [height] that means that the node wants to be as
+     * big as its parent.
+     */
+    public const val MatchParent: Int = -1
+
+    /**
+     * A special constant for [width] or [height] that the node wants to be just large enough
+     * to fit its own internal content, taking its own padding into account.
+     */
+    public const val WrapContent: Int = -2
+
+    /** The default value for the order attribute */
+    public const val DefaultOrder: Int = 1
+
+    /** The default value for the flex grow attribute */
+    public const val DefaultFlexGrow: Float = 0f
+
+    /** The default value for the flex shrink attribute */
+    public const val DefaultFlexShrink: Float = 1f
+
+    /** The value representing the flex shrink attribute is not set */
+    public const val UndefinedFlexShrink: Float = 0f
+
+    /** The default value for the flex basis percent attribute */
+    public const val DefaultFlexBasisPercent: Float = -1f
+  }
+}
diff --git a/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/ObservableMutableList.kt b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/ObservableMutableList.kt
new file mode 100644
index 000000000..ac1cdf600
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/ObservableMutableList.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood
+
+internal class ObservableMutableList<T>(
+  private val onChange: () -> Unit = {},
+) : AbstractMutableList<T>() {
+
+  private val delegate = mutableListOf<T>()
+
+  override val size: Int get() = delegate.size
+
+  override fun get(index: Int): T {
+    return delegate[index]
+  }
+
+  override fun add(index: Int, element: T) {
+    delegate.add(index, element).also { onChange() }
+  }
+
+  override fun set(index: Int, element: T): T {
+    return delegate.set(index, element).also { onChange() }
+  }
+
+  override fun removeAt(index: Int): T {
+    return delegate.removeAt(index).also { onChange() }
+  }
+}
diff --git a/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/properties.kt b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/properties.kt
new file mode 100644
index 000000000..67320400a
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/properties.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * 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:Suppress("MemberVisibilityCanBePrivate")
+
+package app.cash.redwood
+
+import kotlin.jvm.JvmInline
+import kotlin.math.abs
+
+/**
+ * This attribute controls the alignment of the flex lines in the flex container.
+ */
+@JvmInline
+public value class AlignContent(public val ordinal: Int) {
+  init {
+    require(ordinal in 0..5) { "unknown AlignContent: $ordinal" }
+  }
+
+  override fun toString(): String = when (ordinal) {
+    0 -> "FlexStart"
+    1 -> "FlexEnd"
+    2 -> "Center"
+    3 -> "SpaceBetween"
+    4 -> "SpaceAround"
+    5 -> "Stretch"
+    else -> throw AssertionError()
+  }
+
+  public companion object {
+    public val FlexStart: AlignContent = AlignContent(0)
+    public val FlexEnd: AlignContent = AlignContent(1)
+    public val Center: AlignContent = AlignContent(2)
+    public val SpaceBetween: AlignContent = AlignContent(3)
+    public val SpaceAround: AlignContent = AlignContent(4)
+    public val Stretch: AlignContent = AlignContent(5)
+  }
+}
+
+/**
+ * This attribute controls the alignment along the cross axis.
+ */
+@JvmInline
+public value class AlignItems(public val ordinal: Int) {
+  init {
+    require(ordinal in 0..4) { "unknown AlignItems: $ordinal" }
+  }
+
+  override fun toString(): String = when (ordinal) {
+    0 -> "FlexStart"
+    1 -> "FlexEnd"
+    2 -> "Center"
+    3 -> "Baseline"
+    4 -> "Stretch"
+    else -> throw AssertionError()
+  }
+
+  public companion object {
+    public val FlexStart: AlignItems = AlignItems(0)
+    public val FlexEnd: AlignItems = AlignItems(1)
+    public val Center: AlignItems = AlignItems(2)
+    public val Baseline: AlignItems = AlignItems(3)
+    public val Stretch: AlignItems = AlignItems(4)
+  }
+}
+
+/**
+ * This attribute controls the alignment along the cross axis.
+ * The alignment in the same direction can be determined by the
+ * [AlignItems] attribute in the parent, but if this is set to
+ * other than [AlignSelf.Auto], the cross axis alignment is
+ * overridden for this child.
+ */
+@JvmInline
+public value class AlignSelf(public val ordinal: Int) {
+  init {
+    require(ordinal in 0..5) { "unknown AlignSelf: $ordinal" }
+  }
+
+  override fun toString(): String = when (ordinal) {
+    0 -> "FlexStart"
+    1 -> "FlexEnd"
+    2 -> "Center"
+    3 -> "Baseline"
+    4 -> "Stretch"
+    5 -> "Auto"
+    else -> throw AssertionError()
+  }
+
+  public companion object {
+    public val FlexStart: AlignSelf = AlignSelf(0)
+    public val FlexEnd: AlignSelf = AlignSelf(1)
+    public val Center: AlignSelf = AlignSelf(2)
+    public val Baseline: AlignSelf = AlignSelf(3)
+    public val Stretch: AlignSelf = AlignSelf(4)
+    public val Auto: AlignSelf = AlignSelf(5)
+  }
+}
+
+/**
+ * The direction children items are placed inside the flex container, it determines the
+ * direction of the main axis (and the cross axis, perpendicular to the main axis).
+ */
+@JvmInline
+public value class FlexDirection(public val ordinal: Int) {
+  init {
+    require(ordinal in 0..3) { "unknown FlexDirection: $ordinal" }
+  }
+
+  override fun toString(): String = when (ordinal) {
+    0 -> "Row"
+    1 -> "RowReverse"
+    2 -> "Column"
+    3 -> "ColumnReverse"
+    else -> throw AssertionError()
+  }
+
+  public companion object {
+    public val Row: FlexDirection = FlexDirection(0)
+    public val RowReverse: FlexDirection = FlexDirection(1)
+    public val Column: FlexDirection = FlexDirection(2)
+    public val ColumnReverse: FlexDirection = FlexDirection(3)
+  }
+}
+
+/**
+ * This attribute controls whether the flex container is single-line or multi-line,
+ * and the direction of the cross axis.
+ */
+@JvmInline
+public value class FlexWrap(public val ordinal: Int) {
+  init {
+    require(ordinal in 0..2) { "unknown FlexWrap: $ordinal" }
+  }
+
+  override fun toString(): String = when (ordinal) {
+    0 -> "NoWrap"
+    1 -> "Wrap"
+    2 -> "WrapReverse"
+    else -> throw AssertionError()
+  }
+
+  public companion object {
+    public val NoWrap: FlexWrap = FlexWrap(0)
+    public val Wrap: FlexWrap = FlexWrap(1)
+    public val WrapReverse: FlexWrap = FlexWrap(2)
+  }
+}
+
+/**
+ * This attribute controls the alignment along the main axis.
+ */
+@JvmInline
+public value class JustifyContent(public val ordinal: Int) {
+  init {
+    require(ordinal in 0..5) { "unknown JustifyContent: $ordinal" }
+  }
+
+  override fun toString(): String = when (ordinal) {
+    0 -> "FlexStart"
+    1 -> "FlexEnd"
+    2 -> "Center"
+    3 -> "SpaceBetween"
+    4 -> "SpaceAround"
+    5 -> "SpaceEvenly"
+    else -> throw AssertionError()
+  }
+
+  public companion object {
+    public val FlexStart: JustifyContent = JustifyContent(0)
+    public val FlexEnd: JustifyContent = JustifyContent(1)
+    public val Center: JustifyContent = JustifyContent(2)
+    public val SpaceBetween: JustifyContent = JustifyContent(3)
+    public val SpaceAround: JustifyContent = JustifyContent(4)
+    public val SpaceEvenly: JustifyContent = JustifyContent(5)
+  }
+}
+
+/**
+ * A MeasureSpec encapsulates the layout requirements passed from parent to child.
+ * Each MeasureSpec represents a requirement for either the width or the height.
+ * A MeasureSpec is comprised of a size and a mode.
+ */
+@JvmInline
+public value class MeasureSpec private constructor(internal val value: Int) {
+
+  public val size: Int get() = value and 0x3FFF
+  public val mode: MeasureSpecMode get() = MeasureSpecMode(abs(value shr 30))
+
+  public companion object {
+    public const val MaxSize: Int = Int.MAX_VALUE and 0x00FFFFFF
+
+    public fun from(size: Int, mode: MeasureSpecMode): MeasureSpec {
+      require(size in 0..MaxSize) { "invalid size: $size" }
+      // Use the top 2 bits for the mode and use the bottom 30 bits for the size.
+      return MeasureSpec((mode.ordinal shl 30) or (size and 0x3FFF))
+    }
+  }
+}
+
+/**
+ * Denotes how the [MeasureSpec.size] constraint should be interpreted.
+ */
+@JvmInline
+public value class MeasureSpecMode(public val ordinal: Int) {
+  init {
+    require(ordinal in 0..2) { "unknown MeasureSpecMode: $ordinal" }
+  }
+
+  override fun toString(): String = when (ordinal) {
+    0 -> "Unspecified"
+    1 -> "Exactly"
+    2 -> "AtMost"
+    else -> throw AssertionError()
+  }
+
+  public companion object {
+    /**
+     * The parent has not imposed any constraint on the child. It can be whatever size it wants.
+     */
+    public val Unspecified: MeasureSpecMode = MeasureSpecMode(0)
+
+    /**
+     * The parent has determined an exact size for the child. The child is going to be
+     * given those bounds regardless of how big it wants to be.
+     */
+    public val Exactly: MeasureSpecMode = MeasureSpecMode(1)
+
+    /**
+     * The child can be as large as it wants up to the specified size.
+     */
+    public val AtMost: MeasureSpecMode = MeasureSpecMode(2)
+  }
+}
+
+/**
+ * A generic spacing container that can be used for padding or margin.
+ */
+public data class Spacing(
+  val start: Int = 0,
+  val end: Int = 0,
+  val top: Int = 0,
+  val bottom: Int = 0,
+) {
+  init {
+    require(start >= 0 && end >= 0 && top >= 0 && bottom >= 0) {
+      "invalid Spacing: [$start, $end, $top, $bottom]"
+    }
+  }
+
+  public companion object {
+    public val Zero: Spacing = Spacing()
+  }
+}
+
+/**
+ * A two-dimensional size composed of two [Int]s.
+ */
+public data class Size(
+  val width: Int,
+  val height: Int,
+) {
+  init {
+    require(width >= 0 && height >= 0) {
+      "invalid Size: [$width, $height]"
+    }
+  }
+}
diff --git a/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/utils.kt b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/utils.kt
new file mode 100644
index 000000000..c39d82135
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonMain/kotlin/app/cash/redwood/utils.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood
+
+import app.cash.redwood.Node.Companion.MatchParent
+import app.cash.redwood.Node.Companion.WrapContent
+import kotlin.math.max
+import kotlin.math.min
+
+internal fun packLong(lower: Int, higher: Int): Long {
+  return (higher.toLong() shl 32) or (lower.toLong() and 0xFFFFFFFFL)
+}
+
+internal fun unpackLower(value: Long): Int {
+  return value.toInt()
+}
+
+internal fun unpackHigher(value: Long): Int {
+  return (value shr 32).toInt()
+}
+
+/**
+ * The count of the views whose visibilities are not gone in this flex line.
+ */
+internal val Line.itemCountVisible: Int
+  get() = itemCount - invisibleItemCount
+
+/**
+ * Returns true if the main axis is horizontal, false otherwise.
+ */
+internal val FlexDirection.isMainAxisHorizontal: Boolean
+  get() = this == FlexDirection.Row || this == FlexDirection.RowReverse
+
+internal fun MeasureSpec.Companion.getChildMeasureSpec(
+  spec: MeasureSpec,
+  padding: Int,
+  childDimension: Int,
+): MeasureSpec {
+  val size = max(0, spec.size - padding)
+  var resultSize = 0
+  var resultMode = MeasureSpecMode.Unspecified
+  when (val mode = spec.mode) {
+    MeasureSpecMode.Exactly -> if (childDimension >= 0) {
+      resultSize = childDimension
+      resultMode = MeasureSpecMode.Exactly
+    } else if (childDimension == MatchParent) {
+      // Child wants to be our size. So be it.
+      resultSize = size
+      resultMode = MeasureSpecMode.Exactly
+    } else if (childDimension == WrapContent) {
+      // Child wants to determine its own size. It can't be bigger than us.
+      resultSize = size
+      resultMode = MeasureSpecMode.AtMost
+    }
+    MeasureSpecMode.AtMost -> if (childDimension >= 0) {
+      // Child wants a specific size... so be it
+      resultSize = childDimension
+      resultMode = MeasureSpecMode.Exactly
+    } else if (childDimension == MatchParent) {
+      // Child wants to be our size, but our size is not fixed.
+      // Constrain child to not be bigger than us.
+      resultSize = size
+      resultMode = MeasureSpecMode.AtMost
+    } else if (childDimension == WrapContent) {
+      // Child wants to determine its own size. It can't be bigger than us.
+      resultSize = size
+      resultMode = MeasureSpecMode.AtMost
+    }
+    MeasureSpecMode.Unspecified -> if (childDimension >= 0) {
+      // Child wants a specific size... let them have it
+      resultSize = childDimension
+      resultMode = MeasureSpecMode.Exactly
+    } else if (childDimension == MatchParent) {
+      // Child wants to be our size... find out how big it should be
+      resultSize = size
+      resultMode = MeasureSpecMode.Unspecified
+    } else if (childDimension == WrapContent) {
+      // Child wants to determine its own size.... find out how big it should be
+      resultSize = size
+      resultMode = MeasureSpecMode.Unspecified
+    }
+    else -> error("unknown MeasureSpecMode: $mode")
+  }
+  return from(resultSize, resultMode)
+}
+
+internal fun MeasureSpec.Companion.resolveSize(size: Int, measureSpec: MeasureSpec): Int {
+  return when (val mode = measureSpec.mode) {
+    MeasureSpecMode.AtMost -> min(measureSpec.size, size)
+    MeasureSpecMode.Exactly -> measureSpec.size
+    MeasureSpecMode.Unspecified -> size
+    else -> error("unknown MeasureSpecMode: $mode")
+  }
+}
diff --git a/redwood-gradle-plugin/build.gradle b/redwood-gradle-plugin/build.gradle
index f5b868c5d..63c45e8da 100644
--- a/redwood-gradle-plugin/build.gradle
+++ b/redwood-gradle-plugin/build.gradle
@@ -65,6 +65,7 @@ test {
   dependsOn(':redwood-cli:publishAllPublicationsToLocalMavenRepository')
   dependsOn(':redwood-compose:publishAllPublicationsToLocalMavenRepository')
   dependsOn(':redwood-gradle-plugin:publishAllPublicationsToLocalMavenRepository')
+  dependsOn(':redwood-flexbox-engine:publishAllPublicationsToLocalMavenRepository')
   dependsOn(':redwood-protocol:publishAllPublicationsToLocalMavenRepository')
   dependsOn(':redwood-protocol-compose:publishAllPublicationsToLocalMavenRepository')
   dependsOn(':redwood-protocol-widget:publishAllPublicationsToLocalMavenRepository')
diff --git a/redwood-treehouse-composeui/build.gradle b/redwood-treehouse-composeui/build.gradle
index 1a08aa8ab..b260886c8 100644
--- a/redwood-treehouse-composeui/build.gradle
+++ b/redwood-treehouse-composeui/build.gradle
@@ -18,8 +18,4 @@ android {
   buildFeatures {
     compose = true
   }
-
-  composeOptions {
-    kotlinCompilerExtensionVersion libs.versions.androidxComposeCompiler.get()
-  }
 }
diff --git a/samples/todo-list/android-composeui/build.gradle b/samples/todo-list/android-composeui/build.gradle
index e42ade437..8fd973620 100644
--- a/samples/todo-list/android-composeui/build.gradle
+++ b/samples/todo-list/android-composeui/build.gradle
@@ -18,8 +18,4 @@ android {
   buildFeatures {
     compose true
   }
-
-  composeOptions {
-    kotlinCompilerExtensionVersion libs.versions.androidxComposeCompiler.get()
-  }
 }
diff --git a/settings.gradle b/settings.gradle
index ddac23095..191fc63f9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,6 +3,7 @@ rootProject.name = 'redwood'
 include ':redwood-cli'
 include ':redwood-compose'
 include ':redwood-gradle-plugin'
+include ':redwood-flexbox-engine'
 include ':redwood-protocol'
 include ':redwood-protocol-compose'
 include ':redwood-protocol-widget'


Test patch:
diff --git a/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/FakeNode.kt b/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/FakeNode.kt
new file mode 100644
index 000000000..202c24052
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/FakeNode.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood
+
+import app.cash.redwood.Node.Companion.DefaultFlexBasisPercent
+import app.cash.redwood.Node.Companion.DefaultFlexGrow
+import app.cash.redwood.Node.Companion.DefaultFlexShrink
+import app.cash.redwood.Node.Companion.DefaultOrder
+import app.cash.redwood.Node.Companion.WrapContent
+
+class FakeNode(
+  override val width: Int = WrapContent,
+  override val height: Int = WrapContent,
+  override val minWidth: Int = 0,
+  override val minHeight: Int = 0,
+  override val maxWidth: Int = Int.MAX_VALUE,
+  override val maxHeight: Int = Int.MAX_VALUE,
+  override val visible: Boolean = true,
+  override val baseline: Int = -1,
+  override val order: Int = DefaultOrder,
+  override val flexGrow: Float = DefaultFlexGrow,
+  override val flexShrink: Float = DefaultFlexShrink,
+  override val flexBasisPercent: Float = DefaultFlexBasisPercent,
+  override val alignSelf: AlignSelf = AlignSelf.Auto,
+  override val wrapBefore: Boolean = false,
+  override val margin: Spacing = Spacing.Zero,
+) : Node {
+
+  override var measuredWidth: Int = -1
+  override var measuredHeight: Int = -1
+
+  override fun measure(widthSpec: MeasureSpec, heightSpec: MeasureSpec) {
+    measuredWidth = MeasureSpec.resolveSize(width, widthSpec)
+    measuredHeight = MeasureSpec.resolveSize(height, heightSpec)
+  }
+
+  override fun layout(left: Int, top: Int, right: Int, bottom: Int) {}
+}
diff --git a/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/FlexboxEngineTest.kt b/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/FlexboxEngineTest.kt
new file mode 100644
index 000000000..f61e8c5e1
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/FlexboxEngineTest.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+class FlexboxEngineTest {
+
+  private val engine = FlexboxEngine().apply {
+    flexWrap = FlexWrap.Wrap
+    alignItems = AlignItems.Stretch
+    alignContent = AlignContent.Stretch
+  }
+
+  @Test
+  fun testCalculateHorizontalFlexLines() {
+    val view1 = FakeNode(100, 100)
+    val view2 = FakeNode(200, 100)
+    val view3 = FakeNode(300, 100)
+    val view4 = FakeNode(400, 100)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexWrap = FlexWrap.Wrap
+    val widthMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    val heightMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+
+    engine.ensureIndexToFlexLine(engine.nodes.size)
+    val flexLines = engine.calculateHorizontalFlexLines(widthMeasureSpec, heightMeasureSpec)
+
+    assertEquals(3, flexLines.size)
+    assertEquals(300, flexLines[0].mainSize)
+    assertEquals(300, flexLines[1].mainSize)
+    assertEquals(400, flexLines[2].mainSize)
+    assertEquals(100, flexLines[0].crossSize)
+    assertEquals(100, flexLines[1].crossSize)
+    assertEquals(100, flexLines[2].crossSize)
+
+    assertNotNull(engine.indexToFlexLine)
+    assertEquals(0, engine.indexToFlexLine!![0])
+    assertEquals(0, engine.indexToFlexLine!![1])
+    assertEquals(1, engine.indexToFlexLine!![2])
+    assertEquals(2, engine.indexToFlexLine!![3])
+
+    val firstLine = flexLines[0]
+    assertEquals(0, firstLine.firstIndex)
+    assertEquals(1, firstLine.lastIndex)
+    val secondLine = flexLines[1]
+    assertEquals(2, secondLine.firstIndex)
+    assertEquals(2, secondLine.lastIndex)
+    val thirdLine = flexLines[2]
+    assertEquals(3, thirdLine.firstIndex)
+    assertEquals(3, thirdLine.lastIndex)
+  }
+
+  @Test
+  fun testCalculateVerticalFlexLines() {
+    val view1 = FakeNode(100, 100)
+    val view2 = FakeNode(100, 200)
+    val view3 = FakeNode(100, 300)
+    val view4 = FakeNode(100, 400)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexWrap = FlexWrap.Wrap
+    engine.flexDirection = FlexDirection.Column
+    val widthMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+    val heightMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+
+    engine.ensureIndexToFlexLine(engine.nodes.size)
+    val flexLines = engine.calculateVerticalFlexLines(widthMeasureSpec, heightMeasureSpec)
+
+    assertEquals(3, flexLines.size)
+    assertEquals(300, flexLines[0].mainSize)
+    assertEquals(300, flexLines[1].mainSize)
+    assertEquals(400, flexLines[2].mainSize)
+    assertEquals(100, flexLines[0].crossSize)
+    assertEquals(100, flexLines[1].crossSize)
+    assertEquals(100, flexLines[2].crossSize)
+
+    assertNotNull(engine.indexToFlexLine)
+    assertEquals(0, engine.indexToFlexLine!![0])
+    assertEquals(0, engine.indexToFlexLine!![1])
+    assertEquals(1, engine.indexToFlexLine!![2])
+    assertEquals(2, engine.indexToFlexLine!![3])
+
+    val firstLine = flexLines[0]
+    assertEquals(0, firstLine.firstIndex)
+    assertEquals(1, firstLine.lastIndex)
+    val secondLine = flexLines[1]
+    assertEquals(2, secondLine.firstIndex)
+    assertEquals(2, secondLine.lastIndex)
+    val thirdLine = flexLines[2]
+    assertEquals(3, thirdLine.firstIndex)
+    assertEquals(3, thirdLine.lastIndex)
+  }
+
+  @Test
+  fun testDetermineMainSize_direction_row_flexGrowSet() {
+    val view1 = FakeNode(100, 100)
+    val view2 = FakeNode(200, 100, flexGrow = 1.0f)
+    val view3 = FakeNode(300, 100)
+    val view4 = FakeNode(400, 100, flexGrow = 2.0f)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexDirection = FlexDirection.Row
+    engine.flexWrap = FlexWrap.Wrap
+    val widthMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    val heightMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+    engine.flexLines = engine.calculateHorizontalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+
+    assertEquals(100, view1.measuredWidth)
+    assertEquals(100, view1.measuredHeight)
+    // view2 will expand to fill the left space in the first flex line since flex grow is set
+    assertEquals(400, view2.measuredWidth)
+    assertEquals(100, view2.measuredHeight)
+    assertEquals(300, view3.measuredWidth)
+    assertEquals(100, view3.measuredHeight)
+    // view4 will expand to fill the left space in the first flex line since flex grow is set
+    assertEquals(500, view4.measuredWidth)
+    assertEquals(100, view4.measuredHeight)
+  }
+
+  @Test
+  fun testDetermineMainSize_direction_column_flexGrowSet() {
+    val view1 = FakeNode(100, 100)
+    val view2 = FakeNode(100, 200, flexGrow = 1.0f)
+    val view3 = FakeNode(100, 300)
+    val view4 = FakeNode(100, 400, flexGrow = 2.0f)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexDirection = FlexDirection.Column
+    engine.flexWrap = FlexWrap.Wrap
+    val widthMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+    val heightMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    engine.flexLines = engine.calculateVerticalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+
+    assertEquals(100, view1.measuredWidth)
+    assertEquals(100, view1.measuredHeight)
+    assertEquals(100, view2.measuredWidth)
+    // view2 will expand to fill the left space in the first flex line since flex grow is set
+    assertEquals(400, view2.measuredHeight)
+    assertEquals(100, view3.measuredWidth)
+    assertEquals(300, view3.measuredHeight)
+    assertEquals(100, view4.measuredWidth)
+    // view4 will expand to fill the left space in the first flex line since flex grow is set
+    assertEquals(500, view4.measuredHeight)
+  }
+
+  @Test
+  fun testDetermineMainSize_direction_row_flexShrinkSet() {
+    val view1 = FakeNode(200, 100)
+    val view2 = FakeNode(200, 100)
+    val view3 = FakeNode(200, 100)
+    val view4 = FakeNode(200, 100)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexDirection = FlexDirection.Row
+    engine.flexWrap = FlexWrap.NoWrap
+    val widthMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    val heightMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+    engine.flexLines = engine.calculateVerticalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+
+    // Flex shrink is set to 1.0 (default value) for all views.
+    // They should be shrank equally for the amount overflown the width
+    assertEquals(125, view1.measuredWidth)
+    assertEquals(100, view1.measuredHeight)
+    assertEquals(125, view2.measuredWidth)
+    assertEquals(100, view2.measuredHeight)
+    assertEquals(125, view3.measuredWidth)
+    assertEquals(100, view3.measuredHeight)
+    assertEquals(125, view4.measuredWidth)
+    assertEquals(100, view4.measuredHeight)
+  }
+
+  @Test
+  fun testDetermineMainSize_direction_column_flexShrinkSet() {
+    val view1 = FakeNode(100, 200)
+    val view2 = FakeNode(100, 200)
+    val view3 = FakeNode(100, 200)
+    val view4 = FakeNode(100, 200)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexDirection = FlexDirection.Column
+    engine.flexWrap = FlexWrap.NoWrap
+    val widthMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+    val heightMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    engine.flexLines = engine.calculateVerticalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+
+    // Flex shrink is set to 1.0 (default value) for all views.
+    // They should be shrank equally for the amount overflown the height
+    assertEquals(100, view1.measuredWidth)
+    assertEquals(125, view1.measuredHeight)
+    assertEquals(100, view2.measuredWidth)
+    assertEquals(125, view2.measuredHeight)
+    assertEquals(100, view3.measuredWidth)
+    assertEquals(125, view3.measuredHeight)
+    assertEquals(100, view4.measuredWidth)
+    assertEquals(125, view4.measuredHeight)
+  }
+
+  @Test
+  fun testDetermineMainSize_directionRow_fixedSizeViewAndShrinkable_doNotExceedMaxMainSize() {
+    val view1 = FakeNode(100, 100, flexShrink = 0f)
+    val view2 = FakeNode(2000, 2000) // simulate a very long text view
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.flexWrap = FlexWrap.NoWrap
+    val widthMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.AtMost)
+    val heightMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+    engine.flexLines = engine.calculateHorizontalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+
+    // Container with WRAP_CONTENT and a max width forces resizable children to shrink
+    // to avoid exceeding max available space.
+    assertEquals(100, view1.measuredWidth)
+    assertEquals(400, view2.measuredWidth)
+  }
+
+  @Test
+  fun testDetermineMainSize_directionRow_twoFixedSizeViewsAndShrinkable_doNotExceedMaxMainSize() {
+    val view1 = FakeNode(100, 100, flexShrink = 0f)
+    val view2 = FakeNode(2000, 2000) // simulate a very long text view
+    val view3 = FakeNode(100, 100, flexShrink = 0f)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.flexWrap = FlexWrap.NoWrap
+    val widthMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.AtMost)
+    val heightMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Unspecified)
+    engine.flexLines = engine.calculateHorizontalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+
+    // Container with WRAP_CONTENT and a max width forces resizable children to shrink
+    // to avoid exceeding max available space.
+    assertEquals(100, view1.measuredWidth)
+    assertEquals(300, view2.measuredWidth)
+    assertEquals(100, view3.measuredWidth)
+  }
+
+  @Test
+  fun testDetermineCrossSize_direction_row_alignContent_stretch() {
+    val view1 = FakeNode(100, 100)
+    val view2 = FakeNode(200, 100)
+    val view3 = FakeNode(300, 100)
+    val view4 = FakeNode(400, 100)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexDirection = FlexDirection.Row
+    engine.flexWrap = FlexWrap.Wrap
+    engine.alignContent = AlignContent.Stretch
+    val widthMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    val heightMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Exactly)
+    engine.flexLines = engine.calculateHorizontalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+    engine.determineCrossSize(widthMeasureSpec, heightMeasureSpec, 0)
+    engine.stretchViews()
+
+    // align content is set to Align.STRETCH, the cross size for each flex line is stretched
+    // to distribute the remaining free space along the cross axis
+    // (remaining height in this case)
+    assertAlmostEquals(333, view1.measuredHeight)
+    assertAlmostEquals(333, view2.measuredHeight)
+    assertAlmostEquals(333, view3.measuredHeight)
+    assertAlmostEquals(333, view4.measuredHeight)
+  }
+
+  @Test
+  fun testDetermineCrossSize_direction_column_alignContent_stretch() {
+    val view1 = FakeNode(100, 100)
+    val view2 = FakeNode(100, 200)
+    val view3 = FakeNode(100, 300)
+    val view4 = FakeNode(100, 400)
+    engine.nodes += view1
+    engine.nodes += view2
+    engine.nodes += view3
+    engine.nodes += view4
+    engine.flexDirection = FlexDirection.Column
+    engine.flexWrap = FlexWrap.Wrap
+    engine.alignContent = AlignContent.Stretch
+    val widthMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Exactly)
+    val heightMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    engine.flexLines = engine.calculateVerticalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    engine.determineMainSize(widthMeasureSpec, heightMeasureSpec)
+    engine.determineCrossSize(widthMeasureSpec, heightMeasureSpec, 0)
+    engine.stretchViews()
+
+    // align content is set to Align.STRETCH, the cross size for each flex line is stretched
+    // to distribute the remaining free space along the cross axis
+    // (remaining width in this case)
+    assertAlmostEquals(333, view1.measuredWidth)
+    assertAlmostEquals(333, view2.measuredWidth)
+    assertAlmostEquals(333, view3.measuredWidth)
+    assertAlmostEquals(333, view4.measuredWidth)
+  }
+
+  @Test
+  fun testMakeCombinedLong() {
+    var higher = -1
+    var lower = 10
+    var combined = packLong(lower, higher)
+    assertEquals(higher, unpackHigher(combined))
+    assertEquals(lower, unpackLower(combined))
+
+    higher = Int.MAX_VALUE
+    lower = Int.MIN_VALUE
+    combined = packLong(lower, higher)
+    assertEquals(higher, unpackHigher(combined))
+    assertEquals(lower, unpackLower(combined))
+
+    higher = MeasureSpec.from(500, MeasureSpecMode.Exactly).value
+    lower = MeasureSpec.from(300, MeasureSpecMode.Unspecified).value
+    combined = packLong(lower, higher)
+    assertEquals(higher, unpackHigher(combined))
+    assertEquals(lower, unpackLower(combined))
+  }
+
+  @Test
+  fun testMakeMeasureSpec() {
+    var spec = MeasureSpec.from(100, MeasureSpecMode.AtMost)
+    assertEquals(100, spec.size)
+    assertEquals(MeasureSpecMode.AtMost, spec.mode)
+
+    spec = MeasureSpec.from(999, MeasureSpecMode.Exactly)
+    assertEquals(999, spec.size)
+    assertEquals(MeasureSpecMode.Exactly, spec.mode)
+
+    spec = MeasureSpec.from(0, MeasureSpecMode.Unspecified)
+    assertEquals(0, spec.size)
+    assertEquals(MeasureSpecMode.Unspecified, spec.mode)
+  }
+
+  @Test
+  fun testFlexLine_anyItemsHaveFlexGrow() {
+    val view1 = FakeNode(100, 100, flexGrow = 1.0f)
+    val view2 = FakeNode(100, 200)
+    val view3 = FakeNode(100, 300)
+    val view4 = FakeNode(100, 400, flexGrow = 2.0f)
+    engine.apply {
+      nodes += view1
+      nodes += view2
+      nodes += view3
+      nodes += view4
+      flexDirection = FlexDirection.Column
+      flexWrap = FlexWrap.Wrap
+      alignContent = AlignContent.Stretch
+    }
+    val widthMeasureSpec = MeasureSpec.from(1000, MeasureSpecMode.Exactly)
+    val heightMeasureSpec = MeasureSpec.from(500, MeasureSpecMode.Exactly)
+    engine.flexLines = engine.calculateVerticalFlexLines(widthMeasureSpec, heightMeasureSpec)
+    assertEquals(3, engine.flexLines.size)
+    assertTrue(engine.flexLines[0].anyItemsHaveFlexGrow)
+    assertFalse(engine.flexLines[1].anyItemsHaveFlexGrow)
+    assertTrue(engine.flexLines[2].anyItemsHaveFlexGrow)
+  }
+}
diff --git a/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/testUtils.kt b/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/testUtils.kt
new file mode 100644
index 000000000..7f42a8b0a
--- /dev/null
+++ b/redwood-flexbox-engine/src/commonTest/kotlin/app/cash/redwood/testUtils.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood
+
+import kotlin.test.assertTrue
+
+internal fun assertAlmostEquals(expected: Int, actual: Int, tolerance: Int = 1) {
+  val start = expected - tolerance
+  val end = expected + tolerance
+  assertTrue(actual in start..end, message = "$actual is not in the range [$start, $end]")
+}