One Off the Slack: How Do We Have Default Sizes?

Marco Romano asked:

What’s the best API practice for having a composable with a default size but still allow it to be overridden via modifiers? I was thinking something like:

@Composable
fun SomethingNice(
  modifier: Modifier = Modifier,
) {
  Box(
    modifier = modifier.size(24.dp),
  ) {
    // Something more...
  }
}

It is working.

But I was afraid the “default” 24dp modifier would be called after the one passed in via parameter therefore always overriding it. Turns out this is not true. But can’t understand why.

Google’s Adam Powell replied:

Layout sizing modifiers alter the measurement constraints of their element more or less as (pseudocode) min = max = myRequestedSize.coerceIn(parentSuppliedMin, parentSuppliedMax)

If the incoming constraint range doesn’t include the requested size then it will try to get as close as it can

But only the requiredSize modifiers override parent constraints

Marco was mystified:

Not sure I’m following. What I meant was:

If I draw this: SomethingNice() It gets drawn at 24dp as expected.

And if I draw this: SomethingNice(modifier = Modifier.size(64.dp)) It gets drawn at 64dp. But it is strange to me as in the code it will happen something like:

@Composable
fun SomethingNice() {
  Box(
    modifier = Modifier.size(64.dp).size(24.dp),
  ) {
    // Something more...
  }
}

Shouldn’t the second call to .size(24.dp) override the first call to .size(64.dp) ?

The problem lies in size coercion rules. As Adam put it:

24.coerceIn(64, 64) = 64

24 is out of range of the constraints given by size(64.dp) so it’s coerced into the constraints of the upstream parent

Marco got even more confused:

Wait a sec… so by “the upstream parent” you mean “the upstream modifier” OR the “enclosing (i.e. outer) composable”?

So there’s no way to create a composable that would size itself to a default value while at the same time offering the (opt-in) possibility of sizing it to any other value?

But, later, Marco found a possible solution:

In the end swapping modifier = modifier.size(24.dp) with modifier = Modifier.size(24.dp).then(modifier) Solve any issues with sizing I had 🤷

Adam tried again to explain:

You should never need to invert the modifier order that way, you can still change the size externally with the component expressing its own default using the standard ordering.

“upstream parent” means the modifier that comes before it if there is one, or the parent layout composable if there are no more modifiers before it

the layout process is one where each parent (and modifier) tells the next link in the chain, “you must be between min and max size; how big are you?” Modifier.size narrows those min and max constraints to be as close to the requested value as possible, within the constraints of how it was measured by its immediate parent (whether that parent is another layout modifier or a layout composable)

so if I write:

@Composable fun DefaultSizeBox(
  modifier: Modifier = Modifier,
  content: @Composable BoxScope.() -> Unit
) = Box(modifier.size(24.dp), content = content)

and then call it like this:

DefaultSizeBox(Modifier.size(64.dp)) {
  Text("Hello, world")
}

then the final modifier for the Box created by DefaultSizeBox is Modifier.size(64.dp).size(24.dp). Measurement then proceeds; let’s say the parent of the DefaultSizeBox measures it with the constraints (min = 0, max = 500) (simplified to only one dimension here for the sake of this discussion; both width and height each have a min and max)

That measurement operation reaches the first .size(64.dp). 64 is between 0 and 500, so now the measurement proceeds, but with (min = 64, max = 64)

next, measurement reaches .size(24.dp). 24 is not between 64 and 64; in order to be within that range the constraints are “changed” to (min = 64, max = 64). The final box is measured with those constraints, and reports that it is size 64. The “outer” modifier passed to the DefaultSizeBox call thereby overrode the requested default defined in DefaultSizeBox’s implementation.


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