One Off the Slack: Scoping Child Composables

Guy Bieber had a concern:

I have compose functions calling other compose functions. If the invoked function is inside a row and starts with a row, bad things happen.

Guy’s original idea was for a Lint check or something similar to help prevent this from occurring.

Google’s Adam Powell provided a brief explanation of the official approach:

You can declare your composable functions as extensions on the children scopes for those layouts

So, if you want to limit a composable to only be used by an immediate child of a Row, you can declare that composable as an extension function on RowScope:

@Composable
fun RowScope.doSomethingCool() {
  // TODO I said "cool" -- this comment is not "cool"
}

The “receiver” (i.e., value of this) for the trailing children lambda on Row is a RowScope:

@Composable
@OptIn(ExperimentalLayoutNodeApi::class, InternalLayoutApi::class)
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalGravity: Alignment.Vertical = Alignment.Top,
    children: @Composable RowScope.() -> Unit
) {
    val measureBlocks = rowMeasureBlocks(
        horizontalArrangement,
        verticalGravity
    )
    Layout(
        children = { RowScope.children() },
        measureBlocks = measureBlocks,
        modifier = modifier
    )
}

(from the current master source code)

While in theory you could somehow use RowScope elsewhere, most likely you will not, and therefore your RowScope extension function is only usable when defining the children of a Row.

RowScope is simply a marker object. It has no functionality, other than defining some Modifier functions that are supported by Row:

@LayoutScopeMarker
@Immutable
object RowScope {
    /**
     * Position the element vertically within the [Row] according to [align].
     *
     * Example usage:
     * @sample androidx.compose.foundation.layout.samples.SimpleGravityInRow
     */
    @Stable
    fun Modifier.gravity(align: Alignment.Vertical) = this.then(VerticalGravityModifier(align))

    /**
     * Position the element vertically such that its [alignmentLine] aligns with sibling elements
     * also configured to [alignWithSiblings]. [alignWithSiblings] is a form of [gravity],
     * so both modifiers will not work together if specified for the same layout.
     * [alignWithSiblings] can be used to align two layouts by baseline inside a [Row],
     * using `alignWithSiblings(FirstBaseline)`.
     * Within a [Row], all components with [alignWithSiblings] will align vertically using
     * the specified [HorizontalAlignmentLine]s or values provided using the other
     * [alignWithSiblings] overload, forming a sibling group.
     * At least one element of the sibling group will be placed as it had [Alignment.Top] gravity
     * in [Row], and the alignment of the other siblings will be then determined such that
     * the alignment lines coincide. Note that if only one element in a [Row] has the
     * [alignWithSiblings] modifier specified the element will be positioned
     * as if it had [Alignment.Top] gravity.
     *
     * Example usage:
     * @sample androidx.compose.foundation.layout.samples.SimpleRelativeToSiblingsInRow
     */
    @Stable
    fun Modifier.alignWithSiblings(alignmentLine: HorizontalAlignmentLine) =
        this.then(SiblingsAlignedModifier.WithAlignmentLine(alignmentLine))

    /**
     * Size the element's width proportional to its [weight] relative to other weighted sibling
     * elements in the [Row]. The parent will divide the horizontal space remaining after measuring
     * unweighted child elements and distribute it according to this weight.
     * When [fill] is true, the element will be forced to occupy the whole width allocated to it.
     * Otherwise, the element is allowed to be smaller - this will result in [Row] being smaller,
     * as the unused allocated width will not be redistributed to other siblings.
     */
    @Stable
    fun Modifier.weight(
        @FloatRange(from = 0.0, to = 3.4e38 /* POSITIVE_INFINITY */, fromInclusive = false)
        weight: Float,
        fill: Boolean = true
    ): Modifier {
        require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
        return this.then(LayoutWeightImpl(weight, fill))
    }

    /**
     * Position the element vertically such that the alignment line for the content as
     * determined by [alignmentLineBlock] aligns with sibling elements also configured to
     * [alignWithSiblings]. [alignWithSiblings] is a form of [gravity], so both modifiers
     * will not work together if specified for the same layout.
     * Within a [Row], all components with [alignWithSiblings] will align vertically using
     * the specified [HorizontalAlignmentLine]s or values obtained from [alignmentLineBlock],
     * forming a sibling group.
     * At least one element of the sibling group will be placed as it had [Alignment.Top] gravity
     * in [Row], and the alignment of the other siblings will be then determined such that
     * the alignment lines coincide. Note that if only one element in a [Row] has the
     * [alignWithSiblings] modifier specified the element will be positioned
     * as if it had [Alignment.Top] gravity.
     *
     * Example usage:
     * @sample androidx.compose.foundation.layout.samples.SimpleRelativeToSiblings
     */
    @Stable
    fun Modifier.alignWithSiblings(
        alignmentLineBlock: (Measured) -> Int
    ) = this.then(SiblingsAlignedModifier.WithAlignmentLineBlock(alignmentLineBlock))
}

If you have similar restrictions with your custom composables, you could adopt the same pattern:


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!