perf: use multi-content Layout to eliminate all wrapper nodes

Replace Box/Column wrappers with the multi-content Layout overload
(contents: List) which makes each slot's composables direct
measurables. This removes 6 intermediate layout nodes per note item.

Multi-child slots (noteContent, reactionsRow) are now measured and
stacked inline using arrayOfNulls<Placeable> with zero-allocation
iteration, avoiding Column's internal measure pass entirely.

https://claude.ai/code/session_01Fam5VHNaNkKBBafg1gRfFS
This commit is contained in:
Claude
2026-04-05 02:49:42 +00:00
parent 80de923cab
commit 350873b7eb
@@ -20,14 +20,12 @@
*/
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
@@ -47,8 +45,10 @@ private data class NoteLayoutPx(
)
/**
* Custom single-pass layout for note items that replaces nested Row/Column
* measurements with direct constraint calculation using known fixed dimensions.
* Custom zero-overhead layout for note items. Uses the multi-content [Layout]
* overload to eliminate ALL intermediate wrapper nodes (Box/Column). Each slot's
* composables become direct measurables measured with pre-computed constraints
* in a single pass.
*
* Layout structure (when [showAuthorColumn] is true):
* ```
@@ -67,11 +67,12 @@ private data class NoteLayoutPx(
* └───────────────────────────────────────────────┘
* ```
*
* Performance benefits over nested Row/Column:
* - Eliminates Row's two-pass measurement (measure author, then content with remaining width)
* - Uses pre-computed pixel dimensions for author column width (55dp) and gaps
* - Reduces content Column children from ~6 to ~3
* - Single measure pass positions all sections with known constraints
* Performance characteristics:
* - Zero intermediate layout nodes (no Box/Column wrappers)
* - Pre-computed pixel dimensions cached across recompositions
* - Single measure pass with known constraints for all 6 slots
* - Multi-child slots (noteContent, reactionsRow) stacked inline
* - Hidden slots skip measurement entirely (empty measurable lists)
*
* @param addPadding Whether to add standard note padding (12dp sides, 10dp top)
* @param showAuthorColumn Whether to reserve space for the author picture column
@@ -109,22 +110,17 @@ fun NoteComposeLayout(
}
Layout(
content = {
// Slot 0: Author picture (55x55 area)
Box { authorPicture() }
// Slot 1: Relay badges (55dp wide, below author)
Box { relayBadges() }
// Slot 2: First row (author name, time, options)
Box { firstRow() }
// Slot 3: Second row (NIP05, location, PoW)
Box { secondRow() }
// Slot 4: Note content (event-specific + zap splits + approval)
Column(Modifier.fillMaxWidth()) { noteContent() }
// Slot 5: Reactions row (full width)
Column(Modifier.fillMaxWidth()) { reactionsRow() }
},
contents =
listOf(
authorPicture,
relayBadges,
firstRow,
secondRow,
noteContent,
reactionsRow,
),
modifier = modifier,
) { measurables, constraints ->
) { allMeasurables, constraints ->
val maxWidth = constraints.maxWidth
// Compute padding offsets
@@ -138,70 +134,94 @@ fun NoteComposeLayout(
val gap = if (showAuthorColumn) px.authorContentGap else 0
val contentWidth = (innerWidth - authorW - gap).coerceAtLeast(0)
// Build constraints for each section
val authorConstraints =
if (showAuthorColumn) Constraints(maxWidth = px.authorWidth) else ZeroConstraints
// Pre-compute constraint objects once
val authorConstraints = Constraints(maxWidth = px.authorWidth)
val contentConstraints = Constraints(maxWidth = contentWidth)
val fullWidthConstraints = Constraints(maxWidth = maxWidth)
// Measure all children in a single pass with pre-computed constraints
val authorPlaceable = measurables[0].measure(authorConstraints)
val relayPlaceable = measurables[1].measure(authorConstraints)
val firstRowPlaceable = measurables[2].measure(contentConstraints)
// Measure author column (single child per slot, skip if empty)
val authorPlaceable = allMeasurables[0].firstOrNull()?.measure(authorConstraints)
val relayPlaceable = allMeasurables[1].firstOrNull()?.measure(authorConstraints)
// Measure header rows
val firstRowPlaceable = allMeasurables[2].firstOrNull()?.measure(contentConstraints)
val secondRowPlaceable =
measurables[3].measure(
if (showSecondRow) contentConstraints else ZeroConstraints,
)
val contentPlaceable = measurables[4].measure(contentConstraints)
val reactionsPlaceable =
measurables[5].measure(Constraints(maxWidth = maxWidth))
if (showSecondRow) allMeasurables[3].firstOrNull()?.measure(contentConstraints) else null
// Measure note content children directly (no Column wrapper)
val contentMeasurables = allMeasurables[4]
val contentPlaceables = arrayOfNulls<Placeable>(contentMeasurables.size)
var contentStackHeight = 0
for (i in contentMeasurables.indices) {
val placeable = contentMeasurables[i].measure(contentConstraints)
contentPlaceables[i] = placeable
contentStackHeight += placeable.height
}
// Measure reactions row children directly (no Column wrapper)
val reactionsMeasurables = allMeasurables[5]
val reactionsPlaceables = arrayOfNulls<Placeable>(reactionsMeasurables.size)
var reactionsHeight = 0
for (i in reactionsMeasurables.indices) {
val placeable = reactionsMeasurables[i].measure(fullWidthConstraints)
reactionsPlaceables[i] = placeable
reactionsHeight += placeable.height
}
// Calculate section heights
val spacer = if (showContentSpacer) px.contentSpacer else 0
val contentColumnHeight =
firstRowPlaceable.height +
(if (showSecondRow) secondRowPlaceable.height else 0) +
(firstRowPlaceable?.height ?: 0) +
(secondRowPlaceable?.height ?: 0) +
spacer +
contentPlaceable.height
contentStackHeight
val authorColumnHeight =
if (showAuthorColumn) {
authorPlaceable.height + px.authorBadgeGap + relayPlaceable.height
if (showAuthorColumn && authorPlaceable != null) {
authorPlaceable.height + px.authorBadgeGap + (relayPlaceable?.height ?: 0)
} else {
0
}
val mainAreaHeight = maxOf(authorColumnHeight, contentColumnHeight)
val totalHeight = padTop + mainAreaHeight + reactionsPlaceable.height
val totalHeight = padTop + mainAreaHeight + reactionsHeight
layout(maxWidth, totalHeight.coerceAtLeast(constraints.minHeight)) {
// Place author column (inside padding)
// Place author column
if (showAuthorColumn) {
authorPlaceable.placeRelative(padStart, padTop)
relayPlaceable.placeRelative(
authorPlaceable?.placeRelative(padStart, padTop)
relayPlaceable?.placeRelative(
padStart,
padTop + authorPlaceable.height + px.authorBadgeGap,
padTop + (authorPlaceable?.height ?: 0) + px.authorBadgeGap,
)
}
// Place content column (to the right of author, inside padding)
// Place content column
val contentX = padStart + authorW + gap
var y = padTop
firstRowPlaceable.placeRelative(contentX, y)
y += firstRowPlaceable.height
firstRowPlaceable?.placeRelative(contentX, y)
y += firstRowPlaceable?.height ?: 0
if (showSecondRow) {
secondRowPlaceable.placeRelative(contentX, y)
y += secondRowPlaceable.height
secondRowPlaceable?.placeRelative(contentX, y)
y += secondRowPlaceable?.height ?: 0
}
y += spacer
contentPlaceable.placeRelative(contentX, y)
for (placeable in contentPlaceables) {
placeable?.placeRelative(contentX, y)
y += placeable?.height ?: 0
}
// Place reactions row at full width, below main area
reactionsPlaceable.placeRelative(0, padTop + mainAreaHeight)
var reactionsY = padTop + mainAreaHeight
for (placeable in reactionsPlaceables) {
placeable?.placeRelative(0, reactionsY)
reactionsY += placeable?.height ?: 0
}
}
}
}