Sortable Tables & Data Grids in Kotlin Compose Desktop | Lesson 14
Video: Sortable Tables & Data Grids in Kotlin Compose Desktop | Lesson 14 by Taught by Celeste AI - AI Coding Coach
Watch full page →Sortable Tables & Data Grids in Kotlin Compose Desktop
In this lesson, you’ll learn how to build a powerful data explorer using Kotlin Compose Desktop with Material 3 dark theme. The example covers sortable column headers, column resizing with drag gestures, pagination controls, text search, category filtering, and exporting data to CSV and JSON formats.
Code
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class Product(val id: Int, val name: String, val category: String, val price: Double)
enum class SortOrder { Ascending, Descending, None }
data class SortState(val column: String, val order: SortOrder)
@Composable
fun SortableDataGrid(products: List<Product>) {
var sortState by remember { mutableStateOf(SortState("id", SortOrder.None)) }
var columnWidths by remember { mutableStateOf(mapOf("id" to 60.dp, "name" to 150.dp, "category" to 100.dp, "price" to 80.dp)) }
var searchQuery by remember { mutableStateOf("") }
var selectedCategory by remember { mutableStateOf("All") }
var currentPage by remember { mutableStateOf(0) }
val itemsPerPage = 10
// Filter products by search and category
val filtered = products.filter {
(selectedCategory == "All" || it.category == selectedCategory) &&
(it.name.contains(searchQuery, ignoreCase = true) ||
it.category.contains(searchQuery, ignoreCase = true))
}
// Sort products based on sortState
val sorted = when (sortState.order) {
SortOrder.Ascending -> filtered.sortedBy { it.getFieldValue(sortState.column) }
SortOrder.Descending -> filtered.sortedByDescending { it.getFieldValue(sortState.column) }
else -> filtered
}
// Paginate
val paginated = sorted.drop(currentPage * itemsPerPage).take(itemsPerPage)
Column(Modifier.fillMaxSize().padding(16.dp)) {
// Search and Category Filter UI omitted for brevity
// Table Header with sortable columns and resizable widths
Row {
listOf("id", "name", "category", "price").forEach { column ->
Box(
Modifier
.width(columnWidths[column] ?: 100.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
val currentWidth = columnWidths[column] ?: 100.dp
val newWidth = (currentWidth + dragAmount.x.dp).coerceAtLeast(50.dp)
columnWidths = columnWidths.toMutableMap().apply { put(column, newWidth) }
}
}
.clickable {
sortState = when {
sortState.column != column -> SortState(column, SortOrder.Ascending)
sortState.order == SortOrder.Ascending -> SortState(column, SortOrder.Descending)
sortState.order == SortOrder.Descending -> SortState(column, SortOrder.None)
else -> SortState(column, SortOrder.Ascending)
}
}
.padding(8.dp)
) {
Text(text = column.capitalize() + when {
sortState.column == column && sortState.order == SortOrder.Ascending -> " ↑"
sortState.column == column && sortState.order == SortOrder.Descending -> " ↓"
else -> ""
})
}
}
}
// Data Rows
LazyColumn {
itemsIndexed(paginated) { index, product ->
Row {
Text(product.id.toString(), Modifier.width(columnWidths["id"] ?: 60.dp).padding(8.dp))
Text(product.name, Modifier.width(columnWidths["name"] ?: 150.dp).padding(8.dp))
Text(product.category, Modifier.width(columnWidths["category"] ?: 100.dp).padding(8.dp))
Text("$${product.price}", Modifier.width(columnWidths["price"] ?: 80.dp).padding(8.dp))
}
}
}
// Pagination Controls omitted for brevity
}
}
// Helper to get property value by name for sorting
fun Product.getFieldValue(field: String): Comparable<*> = when (field) {
"id" -> id
"name" -> name
"category" -> category
"price" -> price
else -> ""
}
Key Points
- Use LazyColumn with itemsIndexed for efficient, scrollable data grids in Compose Desktop.
- Implement sortable columns by tracking SortState and updating sorting on header clicks.
- Enable column resizing with pointerInput and detectDragGestures, enforcing minimum widths.
- Combine filtering, sorting, and pagination using derived state chains for responsive UI updates.
- Serialize data classes with kotlinx-serialization for exporting to CSV or JSON formats.