feat: Refactor DAO insert methods to use Upsert and add transaction support for field reordering

This commit is contained in:
2025-10-25 15:40:58 +02:00
parent 95b839c791
commit 8d2e110cfd
7 changed files with 130 additions and 84 deletions
@@ -12,10 +12,10 @@ interface FieldDao {
@Query("SELECT * FROM fields WHERE id = :fieldId")
suspend fun getFieldById(fieldId: String): Field?
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertField(field: Field)
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertFields(fields: List<Field>)
@Update
@@ -30,4 +30,17 @@ interface FieldDao {
// Get all fields updated since timestamp, including deleted ones for sync
@Query("SELECT * FROM fields WHERE updatedAt >= :since")
suspend fun getFieldsUpdatedSince(since: Long): List<Field>
// Reorder fields in a single transaction for clarity and consistency
@Transaction
suspend fun reorderFieldsInTransaction(reorderedFields: List<Field>, timestamp: Long) {
reorderedFields.forEachIndexed { index, field ->
updateField(
field.copy(
order = index,
updatedAt = timestamp
)
)
}
}
}
@@ -17,10 +17,10 @@ interface ItemDao {
@Query("SELECT * FROM items WHERE id = :itemId")
suspend fun getItemById(itemId: String): Item?
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertItem(item: Item)
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertItems(items: List<Item>)
@Update
@@ -12,10 +12,10 @@ interface ItemValueDao {
@Query("SELECT * FROM item_values WHERE id = :valueId")
suspend fun getValueById(valueId: String): ItemValue?
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertValue(value: ItemValue)
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertValues(values: List<ItemValue>)
@Update
@@ -17,10 +17,10 @@ interface ListDao {
@Query("SELECT * FROM lists WHERE id = :listId")
suspend fun getListById(listId: String): CollabList?
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertList(list: CollabList)
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun insertLists(lists: List<CollabList>)
@Update
@@ -66,6 +66,10 @@ fun ListDetailScreen(
val fields by viewModel.fields.collectAsState()
val items by viewModel.items.collectAsState()
// Use derivedStateOf to create stable references
val stableFields by remember { derivedStateOf { fields } }
val stableItems by remember { derivedStateOf { items } }
var showManageColumnsDialog by remember { mutableStateOf(false) }
var showAddItemDialog by remember { mutableStateOf(false) }
var itemToEdit by remember { mutableStateOf<ItemWithValues?>(null) }
@@ -88,8 +92,8 @@ fun ListDetailScreen(
val fieldWidths = remember { mutableStateMapOf<String, Dp>() }
// Initialize field widths
LaunchedEffect(fields) {
fields.forEach { field ->
LaunchedEffect(stableFields) {
stableFields.forEach { field ->
if (!fieldWidths.containsKey(field.id)) {
fieldWidths[field.id] = 150.dp
}
@@ -97,8 +101,8 @@ fun ListDetailScreen(
}
// Apply filtering, sorting, and grouping
val processedItems = remember(items, filterField, filterValue, sortField, sortAscending, groupByField) {
var result = items
val processedItems = remember(stableItems, stableFields, filterField, filterValue, sortField, sortAscending, groupByField) {
var result = stableItems
// Apply filter
if (filterField != null && filterValue.isNotBlank()) {
@@ -174,7 +178,7 @@ fun ListDetailScreen(
}
}
) { padding ->
if (fields.isEmpty()) {
if (stableFields.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
@@ -329,7 +333,8 @@ fun ListDetailScreen(
.horizontalScroll(horizontalScrollState),
verticalAlignment = Alignment.CenterVertically
) {
fields.forEach { field ->
stableFields.forEach { field ->
key(field.id) {
FieldHeader(
field = field,
width = fieldWidths[field.id] ?: 150.dp,
@@ -341,9 +346,10 @@ fun ListDetailScreen(
)
}
}
}
// Items list with synchronized scrolling
if (items.isEmpty()) {
if (stableItems.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
@@ -378,7 +384,7 @@ fun ListDetailScreen(
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
groupedItems.forEach { (groupName, groupItems) ->
groupedItems.entries.forEach { (groupName, groupItems) ->
// Show group header if grouping is enabled
if (groupByField != null) {
item(key = "group_$groupName") {
@@ -397,9 +403,12 @@ fun ListDetailScreen(
}
// Show items in the group
items(groupItems, key = { it.item.id }) { itemWithValues ->
items(
items = groupItems,
key = { it.item.id }
) { itemWithValues ->
ItemRow(
fields = fields,
fields = stableFields,
fieldWidths = fieldWidths,
itemWithValues = itemWithValues,
scrollState = horizontalScrollState,
@@ -415,7 +424,7 @@ fun ListDetailScreen(
if (showManageColumnsDialog) {
ManageColumnsDialog(
fields = fields,
fields = stableFields,
onDismiss = { showManageColumnsDialog = false },
onAddField = { name, fieldType, fieldOptions ->
viewModel.addField(name, fieldType, fieldOptions)
@@ -434,7 +443,7 @@ fun ListDetailScreen(
if (showAddItemDialog) {
AddItemDialog(
fields = fields,
fields = stableFields,
onDismiss = { showAddItemDialog = false },
onAdd = { fieldValues ->
viewModel.addItemWithValues(fieldValues)
@@ -446,7 +455,7 @@ fun ListDetailScreen(
// Sort Dialog
if (showSortDialog) {
SortDialog(
fields = fields,
fields = stableFields,
currentSortField = sortField,
currentSortAscending = sortAscending,
onDismiss = { showSortDialog = false },
@@ -461,7 +470,7 @@ fun ListDetailScreen(
// Group Dialog
if (showGroupDialog) {
GroupDialog(
fields = fields,
fields = stableFields,
currentGroupByField = groupByField,
onDismiss = { showGroupDialog = false },
onApply = { newGroupByField ->
@@ -474,7 +483,7 @@ fun ListDetailScreen(
// Filter Dialog
if (showFilterDialog) {
FilterDialog(
fields = fields,
fields = stableFields,
currentFilterField = filterField,
currentFilterValue = filterValue,
onDismiss = { showFilterDialog = false },
@@ -502,7 +511,7 @@ fun ListDetailScreen(
itemToEdit?.let { itemWithValues ->
EditItemDialog(
fields = fields,
fields = stableFields,
itemWithValues = itemWithValues,
onDismiss = { itemToEdit = null },
onUpdate = { fieldValues ->
@@ -586,6 +595,10 @@ fun ItemRow(
scrollState: androidx.compose.foundation.ScrollState,
onClick: () -> Unit
) {
// Map values by fieldId to ensure correct value-column alignment regardless of original order
val valuesByFieldId = remember(itemWithValues.values) {
itemWithValues.values.associateBy { it.fieldId }
}
Row(
modifier = Modifier
.fillMaxWidth()
@@ -593,8 +606,9 @@ fun ItemRow(
.horizontalScroll(scrollState),
verticalAlignment = Alignment.CenterVertically
) {
fields.forEachIndexed { _, field ->
val value = itemWithValues.values.find { it.fieldId == field.id }
fields.forEach { field ->
key(field.id) {
val value = valuesByFieldId[field.id]
val fieldWidth = fieldWidths[field.id] ?: 150.dp
Box(
@@ -1002,6 +1016,7 @@ fun ItemRow(
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -2596,6 +2611,10 @@ fun EditItemDialog(
) {
val fieldValues = remember { mutableStateMapOf<String, String>() }
var showDeleteConfirmation by remember { mutableStateOf(false) }
// Map values by field to avoid linear lookups and ensure consistent mapping when fields reorder
val valuesByFieldId = remember(itemWithValues.values) {
itemWithValues.values.associateBy { it.fieldId }
}
// Initialize field values from existing item
LaunchedEffect(itemWithValues) {
@@ -2647,7 +2666,7 @@ fun EditItemDialog(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(fields, key = { it.id }) { field ->
val itemValue = itemWithValues.values.find { it.fieldId == field.id }
val itemValue = valuesByFieldId[field.id]
if (itemValue != null) {
FieldInput(
field = field,
@@ -3,12 +3,14 @@ package com.collabtable.app.ui.screens
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.room.withTransaction
import com.collabtable.app.data.database.CollabTableDatabase
import com.collabtable.app.data.model.*
import com.collabtable.app.data.repository.SyncRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import java.util.UUID
@@ -35,7 +37,9 @@ class ListDetailViewModel(
private fun loadListData() {
viewModelScope.launch {
database.listDao().getListWithFields(listId).collect { listWithFields ->
database.listDao().getListWithFields(listId)
.debounce(75)
.collect { listWithFields ->
_list.value = listWithFields?.list
_fields.value = listWithFields?.fields ?: emptyList()
}
@@ -90,12 +94,11 @@ class ListDetailViewModel(
createdAt = timestamp,
updatedAt = timestamp
)
database.fieldDao().insertField(newField)
// Create empty ItemValue entries for this new field for all existing items
val existingItems = _items.value
if (existingItems.isNotEmpty()) {
val newValues = existingItems.map { itemWithValues ->
val newValues = if (existingItems.isNotEmpty()) {
existingItems.map { itemWithValues ->
ItemValue(
id = UUID.randomUUID().toString(),
itemId = itemWithValues.item.id,
@@ -104,8 +107,17 @@ class ListDetailViewModel(
updatedAt = timestamp
)
}
} else {
emptyList()
}
// Insert atomically to avoid intermediate inconsistent states
database.withTransaction {
database.fieldDao().insertField(newField)
if (newValues.isNotEmpty()) {
database.itemValueDao().insertValues(newValues)
}
}
performSync()
}
@@ -143,8 +155,9 @@ class ListDetailViewModel(
createdAt = timestamp,
updatedAt = timestamp
)
// Insert item and its values atomically
database.withTransaction {
database.itemDao().insertItem(newItem)
// Create empty values for each field
val values = _fields.value.map { field ->
ItemValue(
@@ -155,7 +168,10 @@ class ListDetailViewModel(
updatedAt = timestamp
)
}
if (values.isNotEmpty()) {
database.itemValueDao().insertValues(values)
}
}
performSync()
}
}
@@ -169,8 +185,9 @@ class ListDetailViewModel(
createdAt = timestamp,
updatedAt = timestamp
)
// Insert item and provided values atomically
database.withTransaction {
database.itemDao().insertItem(newItem)
// Create values for each field with the provided values
val values = _fields.value.map { field ->
ItemValue(
@@ -181,7 +198,10 @@ class ListDetailViewModel(
updatedAt = timestamp
)
}
if (values.isNotEmpty()) {
database.itemValueDao().insertValues(values)
}
}
performSync()
}
}
@@ -211,14 +231,8 @@ class ListDetailViewModel(
fun reorderFields(reorderedFields: List<Field>) {
viewModelScope.launch {
val timestamp = System.currentTimeMillis()
reorderedFields.forEachIndexed { index, field ->
database.fieldDao().updateField(
field.copy(
order = index,
updatedAt = timestamp
)
)
}
// Use DAO-level transaction for clarity
database.fieldDao().reorderFieldsInTransaction(reorderedFields, timestamp)
performSync()
}
}
Binary file not shown.