Exploring the Impact of verticalScroll() on fillMaxHeight() Modifier in Android Compose
Tags: android
Published: 2024-01-13 08:06:42

Encountering a peculiar situation in Android Compose where the `Modifier.verticalScroll()` seems to ignore `Modifier.fillMaxHeight()` when applied to the parent composable sparked my curiosity. Let me share my notes on what I discovered while digging into the internal workings.

## What Caught My Eye
Consider this scenario: without `verticalScroll()` on the parent, the child's `fillMaxHeight()` behaves as expected. 

```kotlin
@Composable
fun Sample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
    ) {
        Text(
            modifier = Modifier.fillMaxHeight(),
            text = "Without verticalScroll",
        )
    }
}
```

![screen shout without verticalScroll()](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqqxtqesiw4kit74ni68.png)

However, once you add `verticalScroll()` to the parent, suddenly `fillMaxHeight()` is overlooked.

```kotlin
@Composable
fun Sample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier.verticalScroll(rememberScrollState())
    ) {
        Text(
            modifier = Modifier.fillMaxHeight(),
            text = "With verticalScroll",
        )
    }
}
```

![screen shout with verticalScroll()](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4ywnn3thfu0z8ates23.png)

I'm eager to understand the specifics of what `verticalScroll()` is doing to cause this shift in behavior.


## Insight from the Official Documentation

[The documentation](https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#(androidx.compose.ui.Modifier).verticalScroll(androidx.compose.foundation.ScrollState,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean)) sheds some light: "Modify element to allow to scroll vertically when height of the content is bigger than max constraints allow."
This suggests that if the content's size relies on the parent's size, determining whether to scroll or not may become challenging, leading to the observed behavior of being ignored.

## Peeking into the Internal Mechanism

Let's take a closer look at what's happening inside `Modifier.verticalScroll()`. Skipping over some details, the crucial point is the utilization of `ScrollingLayoutModifier`.

```kotlin
fun Modifier.verticalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
) = scroll(
    state = state,
    isScrollable = enabled,
    reverseScrolling = reverseScrolling,
    flingBehavior = flingBehavior,
    isVertical = true
)
```

```kotlin
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.scroll(
    state: ScrollState,
    reverseScrolling: Boolean,
    flingBehavior: FlingBehavior?,
    isScrollable: Boolean,
    isVertical: Boolean
) = composed(
    factory = {


        val layout =
            ScrollingLayoutModifier(state, reverseScrolling, isVertical)
        semantics
            .clipScrollableContainer(orientation)
            .overscroll(overscrollEffect)
            .then(scrolling)
            .then(layout)
    },
```

The `ScrollingLayoutModifier()` seems to be the culprit as it's overwriting the child's constraints, setting `maxHeight` to `Infinity`, regardless of the original setting.

```kotlin
private data class ScrollingLayoutModifier(
    val scrollerState: ScrollState,
    val isReversed: Boolean,
    val isVertical: Boolean
) : LayoutModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        checkScrollableContainerConstraints(
            constraints,
            if (isVertical) Orientation.Vertical else Orientation.Horizontal
        )

        val childConstraints = constraints.copy(
            maxHeight = if (isVertical) Constraints.Infinity else constraints.maxHeight,
            maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity
        )
        val placeable = measurable.measure(childConstraints)
        val width = placeable.width.coerceAtMost(constraints.maxWidth)
        val height = placeable.height.coerceAtMost(constraints.maxHeight)



        return layout(width, height) {
            val scroll = scrollerState.value.coerceIn(0, side)
            val absScroll = if (isReversed) scroll - side else -scroll
            val xOffset = if (isVertical) 0 else absScroll
            val yOffset = if (isVertical) absScroll else 0
            placeable.placeRelativeWithLayer(xOffset, yOffset)
        }
    }
```

This means that, with constraints' `maxHeight` set to `Constraints.Infinity`, the child's height becomes unbounded, effectively immune to the influence of the parent. Consequently, it adopts the height of the Text portion within the child.

For more insights into Compose's Layout and Constraints, you might find [this reference](https://developer.android.com/jetpack/compose/layouts/constraints-modifiers) useful.