If I am building a custom layout which takes in 2 ...
# compose
s
If I am building a custom layout which takes in 2 composable slots, I figured that if someone passes
{}
for one of the slots, then trying to measure it will just not find that item at all. So with this
Copy code
fun CustomLayout(
  firstSlot: @Composable () -> Unit,
  secondSlot: @Composable () -> Unit,
) {
  Layout(
    content = {
      firstSlot()
      secondSlot()
    },
  ) { measurable, constraints ->
    val firstPlaceable = measurable[0].measure(constraints)
    val secondPlaceable = measurable[1].measure(
    ...
  }
}
So if someone passes
firstSlot = {}
the first measurable would in fact be whatever secondSlot had inside, and then when trying to access the second measurable this crashes. What is my best bet here to make sure I can avoid that?
My first idea was to do this
Copy code
fun CustomLayout(
  firstSlot: @Composable () -> Unit,
  secondSlot: @Composable () -> Unit,
) {
  Layout(
    content = {
      Box { firstSlot() }
      Box { secondSlot() }
    },
  ) { measurable, constraints ->
    val firstPlaceable = measurable[0].measure(constraints)
    val secondPlaceable = measurable[1].measure(
    ...
  }
}
But this affects the way that the two composables are in fact laid out in the end. I think they lose part of the information of their incoming constraints
I tried doing this then instead, by also ensuring that the min constraints are propagated down
Copy code
fun CustomLayout(
  firstSlot: @Composable () -> Unit,
  secondSlot: @Composable () -> Unit,
) {
  Layout(
    content = {
      Box(propagateMinConstraints = true) { firstSlot() }
      Box(propagateMinConstraints = true) { secondSlot() }
    },
  ) { measurable, constraints ->
    val firstPlaceable = measurable[0].measure(constraints)
    val secondPlaceable = measurable[1].measure(
    ...
  }
}
And I think this has worked well so far. But is there anything else I could be missing here? If anyone else has tried to do this before and has done something differently I would be happy to hear about it!
a
There is also an overload of
Layout
with
contents
parameter which allows you to pass in multiple composable lambdas.
s
I would still need to guard against someone passing in
{}
though right? Doing a naive impl like this
Copy code
Layout(
  modifier = modifier,
  contents = listOf(
    { firstSlot() },
    { secondSlot() },
  ),
) { measurables, constraints ->
  val firstPlaceable = measurables[0][0].measure(constraints)
  val secondPlaceable = measurables[1][0].measure(
}
It will simply find nothing at
[0]
which would be inside the list itself. I'd still then need to do
measurables[0].getOrNull(0)
and
measurables[1].getOrNull(0)
and then fallback in case of null to whatever makes sense in my scenario
Doing the Box impl above allows me to always find something in those indexes, and if I understand correctly, if someone passed in
{}
then it just measures as a placeable with 0 width and height. Which I could I suppose default to (to the 0 width and height that is) in case of null using the list of measurables approach, right?
a
You shouldn't just take the first element of each list. You should iterate through the lists, measure and place all the elements. This is useful if you want to control all the elements yourself. If you just want the behavior of a
Box
, using that is perfectly fine. I'm not sure what you are looking for besides.
l
You can use Modifier.layoutId on the box, and then look for the layoutId on the measurable to match them as well
2
e
You can use Modifier.layoutId …
Wish compose provided better apis than this for this & related use cases 🤦🏾‍♂️
s
Hm yeah that's right, I should measure all of them if there are many of them present. However in my particular case I do expect callers to only ever pass one top-level composable in the slot. And I do control all callers, this is not for a library. What I want the caller to do is either call one single composable in there, or if the want multiple composables they should decide on what the layout logic should be for them. So I want them to do this
Copy code
firstSlot = {
 Column { // or Box, or Row, or whatever they want really 
  One()
  Two()
 }
}
Or this should be allowed too
Copy code
firstSlot = {}
But I never want them to do
Copy code
firstSlot = {
 One()
 Two()
}
I suppose there is no way I could expose that rule through the function signature right? Regarding adding a layoutId, I have done that in the past and it is quite convenient. In this particular case however since I do always wrap them in a box myself there's no way doing [0] and [1] will fail on my measurables list as far as I understand. This would be good enough for the needs of this layout I think. So my problem with adding a wrapping box initially was that it was then altering the constraints that were passed to the slot content itself. In my measure logic for this particular layout I wanted secondSlot to always be at least as tall and as wide as the firstSlot. I was doing that by doing:
Copy code
val firstPlaceable = measurable[0].measure(constraints)
val secondPlaceable = measurable[1].measure(
  constraints.copy(
    minWidth = firstPlaceable.width.coerceAtLeast(constraints.minWidth),
    minHeight = firstPlaceable.height.coerceAtLeast(constraints.minHeight),
  ),
)
Then the second box was measured exactly as I wanted it to, so that's good. However then on the call site, I was expecting whatever I was putting inside secondSlot to also have those minWidth and minHeight constraints. Before I realized I needed the box to guard against the
firstSlot = {}
scenario that worked well, but when I added the box, the way that the box seems to work is that it takes the constraints, respects them, but then for its children it by default gives them a min width/height of 0 So adding the Box + propagateMinConstraints does seem to tick all of my boxes here. • I guard against empty lambda cases. • I know for sure I always have to measure two things, and they are always in positions [0] and [1] • The min size of the second slot is always at least as big as the first slot.
e
If the count of composables is important to you (ie you want to make sure theres only one item in firstSlot) then you have to use
contents
and you cannot wrap in a box! or else you wont have control. If not … theres nothing more you can do for this other than wrap in a box that propagates constraints and give it a layout ID.
…it is quite convenient
Beg to differ, literally one of the most inconvenient APIs in compose for anything but the most trivial custom layout use cases.
s
Yeah again, in this particular case, since I always know there's two of them, and I always put them there, the layout ID would just be extra code for nothing really. And yeah if someone puts more than one items there now it will simply all be put inside the box. That's fine with me, that will obviously look wrong and prompt the caller to add the wrapping layout they actually wanted in the first place.
z
You have to call slot inside the box composable in Layout or you can just make them optional (nullable) and inform the user that they should either provide null value or non-empty composable.