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:
-
Declare the marker object
-
Set the marker object as being the receiver for the
children
function parameter (children: @Composable MyAwesomeScope.() -> Unit
) -
Invoke
children
using that scope (RowScope.children()
) -
Declare “child” composables that extend your custom marker object, so that they cannot easily be used elsewhere
Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!