Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 51 additions & 21 deletions autosizetable/src/main/java/ksnd/autosizetable/AutoSizeTable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -275,36 +278,63 @@ private fun MeasureTable(
items: List<List<@Composable () -> Unit>>,
content: @Composable (TableItemSize) -> Unit,
) {
SubcomposeLayout(modifier = modifier) { constraints ->
val rowCount = items.size
val columnCount = items.first().size
val rowCount = items.size
val columnCount = items.first().size

// State to hold the measured sizes
var tableItemSize by remember {
mutableStateOf(
TableItemSize(
columnWidthSize = List(columnCount) { 0.dp },
rowHeightSize = List(rowCount) { 0.dp }
)
)
}

Layout(
modifier = modifier,
content = {
// Compose all items for measurement
items.forEachIndexed { rowIndex, rowList ->
rowList.forEachIndexed { columnIndex, item ->
Comment on lines +298 to +299
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rowIndex and columnIndex variables are declared but never used within the nested loops. Consider removing these parameters or using underscore (_) to indicate they are intentionally unused.

Suggested change
items.forEachIndexed { rowIndex, rowList ->
rowList.forEachIndexed { columnIndex, item ->
items.forEachIndexed { _, rowList ->
rowList.forEachIndexed { _, item ->

Copilot uses AI. Check for mistakes.
Box {
item()
Comment on lines +300 to +301
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All table items are now composed and measured on every recomposition, even when they haven't changed. The previous SubcomposeLayout approach only composed items as needed. This could significantly worsen performance for large tables. Consider using keys in the composition or reverting to a subcomposition approach if performance is critical.

Suggested change
Box {
item()
androidx.compose.runtime.key(rowIndex to columnIndex) {
Box {
item()
}

Copilot uses AI. Check for mistakes.
}
Comment on lines +300 to +302
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping items in a Box without any modifiers or parameters adds an unnecessary layer of composition. Consider invoking item() directly unless the Box serves a specific purpose.

Suggested change
Box {
item()
}
item()

Copilot uses AI. Check for mistakes.
}
}
// Compose the actual content with current size information
content(tableItemSize)
}
) { measurables, constraints ->
val itemCount = rowCount * columnCount

val maxHeightPerRow = MutableList(rowCount) { 0.dp }
val maxWidthPerColumn = MutableList(columnCount) { 0.dp }

val itemsMeasurable = items.mapIndexed { rowIndex, rowList ->
List(rowList.size) { columnIndex ->
subcompose("${rowIndex}_$columnIndex") {
items[rowIndex][columnIndex]()
}.first().measure(Constraints())
}
// Measure all items (first itemCount measurables)
measurables.take(itemCount).forEachIndexed { index, measurable ->
val placeable = measurable.measure(Constraints())
val rowIndex = index / columnCount
val columnIndex = index % columnCount

val width = placeable.width.toDp()
val height = placeable.height.toDp()

maxWidthPerColumn[columnIndex] = maxOf(maxWidthPerColumn[columnIndex], width)
maxHeightPerRow[rowIndex] = maxOf(maxHeightPerRow[rowIndex], height)
}

items.forEachIndexed { rowIndex, rowList ->
rowList.forEachIndexed { columnIndex, _ ->
val item = itemsMeasurable[rowIndex][columnIndex]
val width = item.width.toDp()
val height = item.height.toDp()
maxWidthPerColumn[columnIndex] = maxOf(maxWidthPerColumn[columnIndex], width)
maxHeightPerRow[rowIndex] = maxOf(maxHeightPerRow[rowIndex], height)
}
// Update state if sizes have changed
val newTableItemSize = TableItemSize(maxWidthPerColumn, maxHeightPerRow)
if (tableItemSize != newTableItemSize) {
tableItemSize = newTableItemSize
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutating state during layout measurement phase can cause infinite recomposition loops. State updates in the measure lambda trigger recomposition, which triggers measurement again. Consider using SideEffect or LaunchedEffect to update state, or restructure to avoid state mutation during layout.

Copilot uses AI. Check for mistakes.
}

val contentPlaceable = subcompose("content") {
content(TableItemSize(maxWidthPerColumn, maxHeightPerRow))
}.first().measure(constraints)
// Measure the content with the constraints
val contentPlaceable = measurables.drop(itemCount).first().measure(constraints)

layout(contentPlaceable.width, contentPlaceable.height) {
// Only place the content, not the measurement items
contentPlaceable.place(0, 0)
Comment on lines +334 to 338
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using .first() after .drop() will throw NoSuchElementException if the content lambda doesn't emit any measurables. This could happen if the content is conditional or empty. Consider using .firstOrNull() with null handling or add a check to ensure at least one content measurable exists.

Suggested change
val contentPlaceable = measurables.drop(itemCount).first().measure(constraints)
layout(contentPlaceable.width, contentPlaceable.height) {
// Only place the content, not the measurement items
contentPlaceable.place(0, 0)
val contentMeasurable = measurables.drop(itemCount).firstOrNull()
if (contentMeasurable != null) {
val contentPlaceable = contentMeasurable.measure(constraints)
layout(contentPlaceable.width, contentPlaceable.height) {
// Only place the content, not the measurement items
contentPlaceable.place(0, 0)
}
} else {
// No content measurable, layout with zero size
layout(0, 0) { }

Copilot uses AI. Check for mistakes.
}
}
Expand Down