Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a table in Jetpack Compose?

I want to create table views, like the one below, to show the data I have.

A header Another header
First row
Second row

I tried using LazyVerticalGrid to achieve it but Jetpack Compose doesn’t allow me to put LazyVerticalGrid inside a vertically scrollable Column.

It’s been two days and I’m really out of idea. Please help.

like image 342
Kevin Avatar asked Jun 26 '21 14:06

Kevin


People also ask

How do you make a scrollable column in jetpack compose?

We can make the Column scrollable by using the verticalScroll() modifier.

What is lazy column in jetpack compose?

A LazyColumn is a vertically scrolling list that only composes and lays out the currently visible items. It's similar to a Recyclerview in the classic Android View system.

Is jetpack easier to compose?

Jetpack Compose is a modern declarative UI Toolkit for Android. Compose makes it easier to write and maintain your app UI by providing a declarative API that allows you to render your app UI without imperatively mutating frontend views.

When should I use LaunchedEffect?

Use LaunchedEffect() when you are using coroutines and need to cancel and relaunch the coroutine every time your parameter changes and it isn't stored in a mutable state. DisposableEffect() is useful when you aren't using coroutines and need to dispose and relaunch the event every time your parameter changes.


3 Answers

As far as I know, there's no built-in component to that. But it's actually easy to do it with LazyColumn and using the same weight for all lines of the same column.
See this example:

First, you can define a cell for your table:

@Composable
fun RowScope.TableCell(
    text: String,
    weight: Float
) {
    Text(
        text = text,
        Modifier
            .border(1.dp, Color.Black)
            .weight(weight)
            .padding(8.dp)
    )
}

Then you can use it to build your table:

@Composable
fun TableScreen() {
    // Just a fake data... a Pair of Int and String
    val tableData = (1..100).mapIndexed { index, item ->
        index to "Item $index" 
    }
    // Each cell of a column must have the same weight. 
    val column1Weight = .3f // 30%
    val column2Weight = .7f // 70%
    // The LazyColumn will be our table. Notice the use of the weights below
    LazyColumn(Modifier.fillMaxSize().padding(16.dp)) {
        // Here is the header
        item {
            Row(Modifier.background(Color.Gray)) {
                TableCell(text = "Column 1", weight = column1Weight)
                TableCell(text = "Column 2", weight = column2Weight)
            }
        }
        // Here are all the lines of your table.
        items(tableData) {
            val (id, text) = it
            Row(Modifier.fillMaxWidth()) {
                TableCell(text = id.toString(), weight = column1Weight)
                TableCell(text = text, weight = column2Weight)
            }
        }
    }
}

Here is the result:

enter image description here

like image 90
nglauber Avatar answered Oct 24 '22 01:10

nglauber


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.

like image 21
chRyNaN Avatar answered Oct 24 '22 00:10

chRyNaN


My solution is not perfect, but at least the horizontal scroll works. The best I have not found and did not come up with. I hope Google releases its own implementation of DataTable. At the moment DataTable is not implemented for Android. To set the width of the columns, you have to calculate their weights, but this calculation is not accurate.

private fun calcWeights(columns: List<String>, rows: List<List<String>>): List<Float> {
    val weights = MutableList(columns.size) { 0 }
    val fullList = rows.toMutableList()
    fullList.add(columns)
    fullList.forEach { list ->
        list.forEachIndexed { columnIndex, value ->
            weights[columnIndex] = weights[columnIndex].coerceAtLeast(value.length)
        }
    }
    return weights
        .map { it.toFloat() }
}


@Composable
fun SimpleTable(columnHeaders: List<String>, rows: List<List<String>>) {

    val weights = remember { mutableStateOf(calcWeights(columnHeaders, rows)) }

    Column(
        modifier = Modifier
            .horizontalScroll(rememberScrollState())
    ) {
        /* HEADER */
        Row(modifier = Modifier.fillMaxWidth()) {
            columnHeaders.forEachIndexed { rowIndex, cell ->
                val weight = weights.value[rowIndex]
                SimpleCell(text = cell, weight = weight)
            }
        }
        /* ROWS  */
        LazyColumn(modifier = Modifier) {
            itemsIndexed(rows) { rowIndex, row ->
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                ) {
                    row.forEachIndexed { columnIndex, cell ->
                        val weight = weights.value[columnIndex]
                        SimpleCell(text = cell, weight = weight)
                    }
                }
            }
        }
    }
}

@Composable
private fun SimpleCell(
    text: String,
    weight: Float = 1f
) {
    val textStyle = MaterialTheme.typography.body1
    val fontWidth = textStyle.fontSize.value / 2.2f // depends of font used(
    val width = (fontWidth * weight).coerceAtMost(500f)
    val textColor = MaterialTheme.colors.onBackground
    Text(
        text = text,
        maxLines = 1,
        softWrap = false,
        overflow = TextOverflow.Ellipsis,
        color = textColor,
        modifier = Modifier
            .border(0.dp, textColor.copy(alpha = 0.5f))
            .fillMaxWidth()
            .width(width.dp + Size.marginS.times(2))
            .padding(horizontal = 4.dp, vertical =  2.dp)
    )
}
like image 1
user2416823 Avatar answered Oct 24 '22 00:10

user2416823