Does `Modifier.widthIn()` work when inside a horiz...
# compose
s
Does
Modifier.widthIn()
work when inside a horizontally scrollable container? I'm seeing some less-than-obvious-to-me behavior.
Copy code
@Preview
@Composable
private fun Working() {
    Row(Modifier.horizontalScroll(rememberScrollState()).padding(16.dp), Arrangement.spacedBy(12.dp)) {
        repeat(2) {
            Box(Modifier.width(300.dp).height(150.dp).background(Color.Red)) {
                Box(Modifier.fillMaxSize().background(Color.Blue))
            }
        }
    }
}

@Preview
@Composable
private fun Failing() {
    Row(Modifier.horizontalScroll(rememberScrollState()).padding(16.dp), Arrangement.spacedBy(12.dp)) {
        repeat(2) {
            Box(Modifier.widthIn(300.dp).height(150.dp).background(Color.Red)) {
                Box(Modifier.fillMaxSize().background(Color.Blue))
            }
        }
    }
}
I would expect those two to produce the same result, as the only difference is the
widthIn(min = 300.dp)
in the latter. However, the first one produces the expected blue box, and the second gives me a red box, as if the innermost `Box`'s
Modifier.fillMaxSize()
didn't matter. Or is there something I'm just not getting here?
s
As a first thought, what I see here is that size(300) sets both min and max constraints to 300, so doing fillMaxSize on the child makes sense that it fills those constraints since it’s clear how to fill them. sizeIn(300) means that it leaves the max height as unspecified, it only know the minimum size. So then doing fillMaxSize would actually here mean that it’d go beyond 300, it’d in fact fill the entire width, so you wouldn’t see the gap nor part of that second item there. And this is exactly what you’d get if you remove the
horizontalScroll
from the row above it
Now then we know that what breaks is the interaction with
horizontalScroll(rememberScrollState())
and not specifying specific max bounds. Maybe somehow even though the min size is 300, since horizontalScroll expects all of its children to have a specified max height (since asking for “max” width doesn’t make sense for a container which has infinite width, since it’s scrollable. And then somehow it decides that you know what? If I don’t know the max size, I won’t even give it a min size 😅 Not 100% how this interaction plays with each other, I am trying to read the docs of horizontalScroll now.
Right, and then it seems like if you change the code to include a text about the current constraints, something like this
Copy code
Row(Modifier.horizontalScroll(rememberScrollState()).padding(16.dp), Arrangement.spacedBy(12.dp)) {
  repeat(2) {
    Box(Modifier.width(300.dp).height(150.dp).background(Color.Red)) {
      Box(Modifier.fillMaxSize().background(Color.Blue))
      BoxWithConstraints {
        Text("${this.minWidth}|${this.maxWidth}")
      }
    }
  }
}
You get this result: Top (width) gives the child a range of 0 - 300 Bottom (widthIn) gives the child a range of 0 - infinity
When the range is 0 to infinity (and you are inside this scrollable container? Maybe this plays a role too) it seems like fillMaxSize() doesn’t know what size to give to that item. And the reason the min size is 0 (instead of 300) is because you’re inside a box now, which has consumed that min size of 300. If you want the child to also keep that 300 min size you can either • Use the parameter
propagateMinConstraints = true
in your outer box, which would do exactly what it says, also give the min 300 width constraint to your child (not changing the max constraints • Use
Modifier.matchParentSize()
on that child component. Which will pass min and max constraints to be exactly 300, since the parent is in fact 300 in this case. This works
Copy code
Row(Modifier.horizontalScroll(rememberScrollState()).padding(16.dp), Arrangement.spacedBy(12.dp)) {
    repeat(2) {
      Box(Modifier.widthIn(300.dp).height(150.dp).background(Color.Red), propagateMinConstraints = true) {
        Box(Modifier.fillMaxSize().background(Color.Blue))
      }
    }
  }
And this works too
Copy code
Row(Modifier.horizontalScroll(rememberScrollState()).padding(16.dp), Arrangement.spacedBy(12.dp)) {
  repeat(2) {
    Box(Modifier.widthIn(300.dp).height(150.dp).background(Color.Red)) {
      Box(Modifier.matchParentSize().background(Color.Blue))
    }
  }
}
s
Wow, thanks for a lengthy and detailed response! I had, in fact, simplified my use case for brevity, but it's practically the same. The main difference was the level of nesting and that the outermost container was a
Card
, not a
Box
. That matters because
Card
does exactly what you suggest (through
Surface
), and set
propagateMinConstraints = true
. However, it wasn't enough for even deeper nesting, it seems. Since
matchParentSize()
is also on the
BoxScope
, it's not something I would be able to use in this case, either. But I've had a chat with some of my team mates, and we've decided to actually roll with a set
width
, and not let it expand horizontally, but rather grow vertically if needed. My use case was trying to avoid breaking some text if it was too long, but we decided to just break it. Secondly, we were also using
weight
further into the component, which was where the problem initially started.
Copy code
Row(Modifier.horizontalScroll()) {
  Box(Modifier.fillMaxWidth()) {
    Row(Modifier.fillMaxWidth()) {
      Stuff(Modifier.weight(1f))

      OtherStuff(Modifier.size(100.dp))
    }
  }
}
Now, because the inner-most
Row
there didn't get a proper max width constraint (which makes sense, as you say, because the top-level scrollable container is essentially infinitely wide),
weight(1f)
gives a width of
0.dp
, as there's no excess left after
OtherStuff
.
s
Oh yeah using weight + infinitely long content is a no go, it takes 0 size if it does indeed have more content than is available to you. Nice, glad you got it working for you with the approach that you did, it's always fun to learn about these interactions 😅 that's why I even tried to see how this could be solved, you never know when I might need to do something similar