An implementation that supports both fixed and variable column widths, and is horizontally and vertically scrollable would look like the following:
@Composable
fun Table(
modifier: Modifier = Modifier,
rowModifier: Modifier = Modifier,
verticalLazyListState: LazyListState = rememberLazyListState(),
horizontalScrollState: ScrollState = rememberScrollState(),
columnCount: Int,
rowCount: Int,
beforeRow: (@Composable (rowIndex: Int) -> Unit)? = null,
afterRow: (@Composable (rowIndex: Int) -> Unit)? = null,
cellContent: @Composable (columnIndex: Int, rowIndex: Int) -> Unit
) {
val columnWidths = remember { mutableStateMapOf<Int, Int>() }
Box(modifier = modifier.then(Modifier.horizontalScroll(horizontalScrollState))) {
LazyColumn(state = verticalLazyListState) {
items(rowCount) { rowIndex ->
Column {
beforeRow?.invoke(rowIndex)
Row(modifier = rowModifier) {
(0 until columnCount).forEach { columnIndex ->
Box(modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val existingWidth = columnWidths[columnIndex] ?: 0
val maxWidth = maxOf(existingWidth, placeable.width)
if (maxWidth > existingWidth) {
columnWidths[columnIndex] = maxWidth
}
layout(width = maxWidth, height = placeable.height) {
placeable.placeRelative(0, 0)
}
}) {
cellContent(columnIndex, rowIndex)
}
}
}
afterRow?.invoke(rowIndex)
}
}
}
}
}
The benefit of this implementation is that offers greater flexibility and a relatively simple API. However, there are known caveats, including: variable width columns are less performant and column dividers would have to be done on the cell level.
For variable width columns, it is not as performant as it would require multiple "layout passes". Essentially, this implementation lays out each Row in a LazyColumn, measuring each column cell in each row and storing the largest width value in a MutableState. When a larger width value is encountered for a column, it would trigger a recompose, adjusting all of the other cells in that column to be the larger width. This way, every cell in a column has the same width (and every cell in a row has the same height). For fixed width columns, the performance should be equivalent to other implementations as it doesn't require multiple "layout passes".
Usage:
Table(
modifier = Modifier.matchParentSize(),
columnCount = 3,
rowCount = 10,
cellContent = { columnIndex, rowIndex ->
Text("Column: $columnIndex; Row: $rowIndex")
})
Overloaded composable functions that use the above implementation should be fairly trivial to create, such as having a "header row" be the first row in the table.