My team and I don't always remember to size an ima...
# compose
c
My team and I don't always remember to size an image "correctly". With that I mean that the AsyncImage() (using Coil) doesn't have a defined size before it comes back from the network. Essentially we want to require a set size or a maxWidth with aspectRatio modifier. This helps us cut out Cumulative Layout Shift https://web.dev/cls/ (basically content shifting because an image loaded) We want to accomplist this by writing a lint rule (we're working on it) and potentially just enforcing to use OurImageComposable() that will wrap AsyncImage and itself it'll do some checks to make sure that the image bounds can actually be laid out fully without an image to come back from the network. How would you go about checking to make sure that the image has a deterministic height and width? My first attempt at this is basically removing the ability to set any modifier, and requiring that you either set a width + height with args and providing another version of the image that takes in args for fill width OR height + aspect ratio. Thoughts?
Here is what I came up with for now on my first go at it. It actually works better than expected because I thought I would always get initial size of 0.
Copy code
@Composable
fun MyImageComposable(modifier: Modifier) {
    AsyncImage(model = "<https://image>", contentDescription = null,
        modifier=modifier.onGloballyPositioned { layoutCoordinates ->
            val width = layoutCoordinates.size.width
            val hegiht = layoutCoordinates.size.height
            if (width == 0 || hegiht == 0){
                throw RuntimeException("You will have layout shift! Please add a size")
            }
        })
}
a
You can just create a modifier. Something like this:
Copy code
fun Modifier.enforceFixedSize(): Modifier = layout { measurable, constraints ->
    check(constraints.hasFixedWidth && constraints.hasFixedHeight) { "Non-fixed size!" }
    val placeable = measurable.measure(constraints)
    layout(placeable.width, placeable.height) {
        placeable.placeRelative(0, 0)
    }
}
c
albert any difference between my impl and yours? If I go with yours I'd probably do this
Copy code
@Composable
fun MyImageComposable(modifier: Modifier) {
    AsyncImage(model = "<https://image>", contentDescription = null,
        modifier=modifier.enforceFixedSize())
}
but just curious if your layout {} has a benefit over my onGloballyPositioned
a
layout is more efficient than onGloballyPositioned. Plus you can verify the composable has a fixed size rather than a non-zero size (non-zero size doesn’t necessarily mean no CLS).
Also you don’t need a composable. Applying the modifier is enough.
c
awesome. love it. This should definitely help us not shoot ourselves in the foot
kinda on a tangent on this, but @Albert Chang it looks like the
check()
doesn't show the actual callsite of the composable in the stack, just the modifier itself. any ideas on how to get the actual composable thats usuing this modifier to be in the stacktrace?
a
You can add a throwable parameter to the modifier but I don’t think it’s worth as it can greatly affect performance even in release builds. Are there so many `AsyncImage`s in a single screen that manually checking is impossible?
c
Hm. I wonder if I can make the modifier debug only. It's not necessarily that it'd be tough to figure out in the future, but right now I'm trying to add it to all screens/usages and on a few screens things are just crashing (which is great. because it means we've found an issue!) but yeah. Was just curious if there was something I was missing here. Thanks
Hey Albert. I started getting a random crash and have traced it down to this minimal repro case.
Copy code
@Composable
fun MyComposable() {
  Row(modifier = Modifier.fillMaxWidth()) {
    val imageUrl = "<https://someimageurl.com/img.png>"
    AsyncImage(
        model = imageUrl,
        contentDescription = null,
        modifier = Modifier.fillMaxWidth().aspectRatio(200 / 70F).enforceFixSize(),
    )
  }
}
Seems pretty standard. But this will crash. If I remove the Row() then everything works fine. Any ideas? I'm assuming it has something to do with the composable that actually holds MyComposable. But in itself this MyCOmposable looks pretty sane right? or have i been looking at this too long?