Bridging the Gap: Making Jetpack Compose Row Behave Like SwiftUi & Web

Joe RoskopfJoe Roskopf
5 min read

If you've ever tried to compare the behavior of Row in Google's Jetpack Compose, HStack in Apple's SwiftUi, and something like display: flex in the web world, you'll see some interesting results.

First, let's take a look at how HStack renders two long text elements.

We can see SwiftUi has a fairly sane implementation. HStack does its best to try and render the two long text elements.

Now, let's take a look at how web handles the same situation.

Also, a fairly sane implementation. We can see that our similar implementation in web does its best to render both text elements.

Lastly, let's take a look at how Jetpack Compose handles a similar situation

Hmm, that's not similar to other platforms out of the box behavior. We see the first text image completely render, but the second one is pushed off the screen and has a width of 0.

The expert Android developers out there are probably saying, just apply a weight to each element!

Okay, now we are getting somewhere. But what if the text elements aren't roughly the same size?

Obviously that doesn't work the way we want. The reality is, in most scenarios with static layouts, using weights will be fine enough to get you the layouts you want.

What if our layouts are dynamic though? How can we achieve a similar behavior to SwiftUi and web?

Custom Layouts For Fun And For Profit

Before we dive into the solution that worked for me in my situation, let's examine prior art.

Here is an example from Hedvig Insurance of a custom layout that allows two items to take up the amount of space that they want while still respecting the other element.

From the documentation:

When two items need to be laid out horizontally in a row, they can't know how much space they need to take out, which more often than not results in the starting item taking up all the width it needs, squeezing the end item. This layout makes sure to measure their max intrinsic width and give as much space as possible to each item, without squeezing the other one. If both of them were to need more than half of the space, or less than half of the space, they're simply given half of the width each.

This solution works perfectly fine if that is exactly your requirement, but in my particular use case, I still needed to support all of the fun things a normal Row did: weight, alignment, arrangement, etc.

So, for better or worse, the solution I landed on was to copy the Row implementation and make changes to the measure policy of Row to add the behavior I wanted to create.

Examine The Problem

The behavior that is problematic lies in this line of code.

When a child element in the Row is asking for how much room it can have, it takes the remaining amount (forcing it to be at least 0). This exactly describes the problematic behavior from the examples in our intro. When the first element lays out, it takes up as much room as it wants, and then when the second element gets its constraints, the remaining amount of space left is 0, so it doesn't have anywhere to go in the layout.

This area will be our main focus point for our custom solution.

The logic I wanted to follow was something like this:

Grab the max width the current element want to use

Then, calculate the largest part an element could possibly occupy, which is proportional to the max width of the container divided by the number of elements. For example, if you have a Row and there are 5 elements, then assuming all 5 elements want to render at their max width, the largest part an element could take up is 1/5 of the container size (also accounts for elements with a weight)

If the max width that I want to take up is larger than the largest part that I could take up, then I need to calculate how much space I can take up (assuming the other elements don't want max space)

This is done by subtracting the remaining desired width of the other elements, and the elements that have already been laid out, from the max width I want to take up.

In code form, that translates to the following:

// grab the max intrinsix width of elements
val results = IntArray(measurables.size) { 0 }
for (i in startIndex until endIndex) {
    results[i] = measurables[i].maxIntrinsicWidth(0).coerceAtMost(mainAxisMax)
}

val maxWidthIWant = results[i]
val largestPartDividend =
    if (results.all { it == 0 }) {
        // if row is all images or something that has no intrinsic size
        // and we don't know it ahead of time
        measurables.size.coerceAtLeast(1)
    } else {
        measurables.filter { it.rowColumnParentData.weight <= 0f }.size.coerceAtLeast(1)
    }
// largest part is determined by the max width of the container divided by the number of elements that don't have weight
val largestPart = mainAxisMax / largestPartDividend

if (maxWidthIWant > largestPart) {
    val remainingDesiredWidthAfterThisElement = results.drop(i + 1).sum()
    val alreadyConsumedWidth = mainAxisMax - remaining
    val maxWidthICanTakeUp =
        (
            maxWidthIWant - remainingDesiredWidthAfterThisElement -
                alreadyConsumedWidth
        ).coerceAtLeast(
            largestPart,
        )
    remaining.coerceAtLeast(0).coerceAtMost(maxWidthICanTakeUp)
} else {
    remaining.coerceAtLeast(0).coerceAtMost(largestPart)
}

Please note, I haven't fully benchmarked this solution (yet). At best, it's the same as row, but at worse, it's performing additional maxIntrinsicWidth() calls, so I'd imagine there is a performance hit that I haven’t had a chance to fully benchmark yet. If performance is critical, I'd verify it works for your use case first.

Wrapping Up

After implementing my custom row which I called AdaptiveRow, we are now able to layout elements in our row with behavior similar to other platforms.

Prior Art

  1. https://github.com/HedvigInsurance/android/blob/develop/app%2Fdesign-system%2Fdesign-system-hedvig%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Fhedvig%2Fandroid%2Fdesign%2Fsystem%2Fhedvig%2FHorizontalItems.kt#L10-L100

Cover Photo by Karsten Winegeart on Unsplash

0
Subscribe to my newsletter

Read articles from Joe Roskopf directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joe Roskopf
Joe Roskopf

Hey! I’m Joe! I’m an Android developer who is passionate about the human side of software development.