DEV Community

myougaTheAxo
myougaTheAxo

Posted on

SubcomposeLayout Complete Guide — Dynamic Layout Based on Measurement Results

What You'll Learn

This article explains SubcomposeLayout (measure results → control composition, dynamic sizing, custom layouts, performance notes).


What is SubcomposeLayout

Normal Layout measures and composes children simultaneously. SubcomposeLayout can use measurement results to control composition of other children.

@Composable
fun MeasureThenDecide(
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (IntSize) -> Unit
) {
    SubcomposeLayout { constraints ->
        val mainPlaceables = subcompose("main") { mainContent() }
            .map { it.measure(constraints) }

        val mainSize = IntSize(
            mainPlaceables.maxOf { it.width },
            mainPlaceables.sumOf { it.height }
        )

        val dependentPlaceables = subcompose("dependent") { dependentContent(mainSize) }
            .map { it.measure(constraints) }

        val totalHeight = mainSize.height + dependentPlaceables.sumOf { it.height }

        layout(constraints.maxWidth, totalHeight) {
            var y = 0
            mainPlaceables.forEach { it.placeRelative(0, y); y += it.height }
            dependentPlaceables.forEach { it.placeRelative(0, y); y += it.height }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Equal Height Columns

@Composable
fun EqualHeightColumns(
    leftContent: @Composable () -> Unit,
    rightContent: @Composable () -> Unit
) {
    SubcomposeLayout { constraints ->
        val halfWidth = constraints.maxWidth / 2
        val halfConstraints = constraints.copy(maxWidth = halfWidth)

        val leftPlaceables = subcompose("left") { leftContent() }
            .map { it.measure(halfConstraints) }
        val rightPlaceables = subcompose("right") { rightContent() }
            .map { it.measure(halfConstraints) }

        val maxHeight = maxOf(
            leftPlaceables.maxOfOrNull { it.height } ?: 0,
            rightPlaceables.maxOfOrNull { it.height } ?: 0
        )

        layout(constraints.maxWidth, maxHeight) {
            leftPlaceables.forEach { it.placeRelative(0, 0) }
            rightPlaceables.forEach { it.placeRelative(halfWidth, 0) }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Text Width Matching Indicator

@Composable
fun TextWithMatchingIndicator(text: String, isSelected: Boolean) {
    SubcomposeLayout(Modifier.padding(horizontal = 16.dp)) { constraints ->
        val textPlaceable = subcompose("text") {
            Text(
                text,
                style = MaterialTheme.typography.titleMedium,
                fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
            )
        }.first().measure(constraints)

        val indicatorPlaceable = subcompose("indicator") {
            if (isSelected) {
                Box(
                    Modifier
                        .width(with(LocalDensity.current) { textPlaceable.width.toDp() })
                        .height(3.dp)
                        .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(1.5.dp))
                )
            }
        }.firstOrNull()?.measure(constraints)

        val totalHeight = textPlaceable.height + (indicatorPlaceable?.height ?: 0)

        layout(textPlaceable.width, totalHeight) {
            textPlaceable.placeRelative(0, 0)
            indicatorPlaceable?.placeRelative(0, textPlaceable.height)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance Notes

// ❌ SubcomposeLayout is heavier than normal Layout
// Use Modifier for simple size specifications

// ✅ Use Modifier when possible
Box(Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
    Row {
        Column(Modifier.weight(1f).fillMaxHeight()) { /* left */ }
        Column(Modifier.weight(1f).fillMaxHeight()) { /* right */ }
    }
}

// Use SubcomposeLayout only when:
// - Need to compose child B based on measurement of child A
// - Need to dynamically change number of Composables
Enter fullscreen mode Exit fullscreen mode

Summary

Feature API
Dynamic composition SubcomposeLayout
Measure child subcompose().measure()
Positioning layout { placeRelative() }
Simple alternative IntrinsicSize
  • Use SubcomposeLayout for 2-stage measurement→composition control
  • Perfect for equal-height columns, text-width dependent layouts
  • Prefer normal Layout or IntrinsicSize when they work
  • Optimize recomposition with subcompose slot IDs

8 production-ready Android app templates (custom layouts included) are available.

Browse templatesGumroad

Related articles:

  • Custom Layout
  • ConstraintLayout
  • Canvas drawing

Ready-Made Android App Templates

8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.

Browse templatesGumroad

Top comments (0)