fix: scroll to newly added item

This commit is contained in:
Rafal Wisniewski
2026-05-04 15:12:19 +02:00
parent bf9309a155
commit 5fb54bf18e
4 changed files with 111 additions and 83 deletions

View File

@@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.Flow
interface ExpenseDao { interface ExpenseDao {
@Upsert @Upsert
suspend fun insert(expense: Expense) suspend fun insert(expense: Expense): Long
@Transaction @Transaction

View File

@@ -25,8 +25,8 @@ class ExpenseRepository @Inject constructor(
} }
@WorkerThread @WorkerThread
suspend fun save(expense: Expense) { suspend fun save(expense: Expense): Long {
expenseDao.insert(expense) return expenseDao.insert(expense)
} }
@WorkerThread @WorkerThread

View File

@@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -99,15 +100,22 @@ fun ListExpenseScreen(
val expensesFlow = val expensesFlow =
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter) expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter)
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState() val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
var idToScroll by remember { mutableIntStateOf(-1) }
ListExpenseScreen( ListExpenseScreen(
currentTrip = currentTrip, currentTrip = currentTrip,
expensesFlow = expensesFlow, expensesFlow = expensesFlow,
onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) }, onSaveExpense = {
expenseAndCategoryViewModel.save(
it,
currentTrip!!,
onComplete = { id -> idToScroll = id })
},
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) }, onDeleteExpense = { expenseAndCategoryViewModel.delete(it) },
isRecalculatingRate = isRecalculatingRate, isRecalculatingRate = isRecalculatingRate,
initialAutoOpen = initialAutoOpen, initialAutoOpen = initialAutoOpen,
onAutoOpenConsumed = onAutoOpenConsumed onAutoOpenConsumed = onAutoOpenConsumed,
idToScroll = idToScroll
) )
} }
@@ -121,7 +129,8 @@ fun ListExpenseScreen(
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit, onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit,
isRecalculatingRate: Boolean, isRecalculatingRate: Boolean,
initialAutoOpen: Boolean, initialAutoOpen: Boolean,
onAutoOpenConsumed: () -> Unit onAutoOpenConsumed: () -> Unit,
idToScroll: Int
) { ) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@@ -151,7 +160,12 @@ fun ListExpenseScreen(
{ {
Box { Box {
if (items.itemCount == 0) { if (items.itemCount == 0) {
Box(modifier = Modifier.fillMaxSize().padding(10.dp), contentAlignment = Alignment.Center) { Box(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
contentAlignment = Alignment.Center
) {
val textToShow = if (currentTrip == null || currentTrip.isDummy()) { val textToShow = if (currentTrip == null || currentTrip.isDummy()) {
stringResource(string.no_trip_picked) stringResource(string.no_trip_picked)
} else { } else {
@@ -167,85 +181,93 @@ fun ListExpenseScreen(
} }
} else { } else {
LazyColumn( LaunchedEffect(idToScroll) {
modifier = Modifier if (idToScroll == -1) return@LaunchedEffect
.fillMaxSize() for (index in 0 until items.itemCount) {
.semantics { val item = items.peek(index)
contentDescription = "expensesList" if (item is ExpenseListItemUi.Item && item.expenseDto.expense.id == idToScroll) {
}, listState.animateScrollToItem(index)
horizontalAlignment = Alignment.CenterHorizontally, break
state = listState
) {
items(
count = items.itemCount,
key = items.itemKey { item ->
when (item) {
is ExpenseListItemUi.Item -> item.expenseDto.expense.id
is ExpenseListItemUi.Header -> "header_${item.date}"
}
} }
) { index ->
when (val item = items[index]) {
is ExpenseListItemUi.Header -> {
CustomDivider(
date = item.date,
sum = item.sum,
currency = item.currency
)
}
is ExpenseListItemUi.Item -> {
SwipeToDeleteExpenseCard(
expenseDto = item.expenseDto,
onDelete = { expense -> itemToDelete = expense },
onClick = { expenseDto ->
expenseDtoToEdit = expenseDto
showBottomSheet = true
}
)
}
null -> {}
}
Spacer(Modifier.height(10.dp))
} }
} }
} }
LazyColumn(
modifier = Modifier
.fillMaxSize()
.semantics {
contentDescription = "expensesList"
},
horizontalAlignment = Alignment.CenterHorizontally,
state = listState
) {
items(
count = items.itemCount,
key = items.itemKey { item ->
when (item) {
is ExpenseListItemUi.Item -> item.expenseDto.expense.id
is ExpenseListItemUi.Header -> "header_${item.date}"
}
}
) { index ->
when (val item = items[index]) {
is ExpenseListItemUi.Header -> {
CustomDivider(
date = item.date,
sum = item.sum,
currency = item.currency
)
}
} is ExpenseListItemUi.Item -> {
SwipeToDeleteExpenseCard(
expenseDto = item.expenseDto,
onDelete = { expense -> itemToDelete = expense },
onClick = { expenseDto ->
expenseDtoToEdit = expenseDto
showBottomSheet = true
}
)
}
null -> {}
}
Spacer(Modifier.height(10.dp))
if (itemToDelete != null) {
DeleteConfirmationDialog(
onConfirm = {
onDeleteExpense(itemToDelete!!)
itemToDelete = null
},
onCancel = {
itemToDelete = null
} }
)
}
} }
if (showBottomSheet) { }
AddExpenseBottomSheet(
onSave = { expense -> if (itemToDelete != null) {
onSaveExpense(expense) DeleteConfirmationDialog(
showBottomSheet = false onConfirm = {
expenseDtoToEdit = null onDeleteExpense(itemToDelete!!)
}, itemToDelete = null
onDismiss = { },
expenseDtoToEdit = null onCancel = {
showBottomSheet = false itemToDelete = null
}, }
expenseDtoToEdit = expenseDtoToEdit, )
state = sheetState }
)
} if (showBottomSheet) {
AddExpenseBottomSheet(
onSave = { expense ->
onSaveExpense(expense)
showBottomSheet = false
expenseDtoToEdit = null
},
onDismiss = {
expenseDtoToEdit = null
showBottomSheet = false
},
expenseDtoToEdit = expenseDtoToEdit,
state = sheetState
)
} }
} }
@@ -504,7 +526,8 @@ fun PreviewListExpenseScreen() {
onDeleteExpense = {}, onDeleteExpense = {},
isRecalculatingRate = true, isRecalculatingRate = true,
false, false,
{} {},
0
) )
} }
@@ -529,7 +552,8 @@ fun PreviewListExpenseScreenWithoutExpenses() {
onDeleteExpense = {}, onDeleteExpense = {},
isRecalculatingRate = true, isRecalculatingRate = true,
false, false,
{} {},
0
) )
} }
@@ -548,7 +572,8 @@ fun PreviewListExpenseScreenWithoutTrip() {
onDeleteExpense = {}, onDeleteExpense = {},
isRecalculatingRate = true, isRecalculatingRate = true,
false, false,
{} {},
0
) )
} }

View File

@@ -99,14 +99,15 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
expenseRepo.getExpensesDto(tripId, search, filter) expenseRepo.getExpensesDto(tripId, search, filter)
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun save(expense: Expense, trip: Trip) { fun save(expense: Expense, trip: Trip, onComplete: (Int) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
val rate = exchangeRateRepository.getRate( val rate = exchangeRateRepository.getRate(
Currencies.valueOf(expense.currency), Currencies.valueOf(expense.currency),
Currencies.valueOf(trip.currency), Currencies.valueOf(trip.currency),
expense.datetime.toLocalDate() expense.datetime.toLocalDate()
) )
expenseRepo.save(expense.copy(rate = rate)) val id = expenseRepo.save(expense.copy(rate = rate))
onComplete(id.toInt())
} }
} }
@@ -210,7 +211,9 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
} }
.sortedByDescending { it.day } .sortedByDescending { it.day }
val highestAmount = summaryPerDayRaw.maxOf { it.amount }
val highestAmount =
if (summaryPerDayRaw.isEmpty()) 1.0 else summaryPerDayRaw.maxOf { it.amount }
summaryPerDayRaw.map { summaryPerDayRaw.map {
it.copy(percent = ((it.amount / highestAmount)).toFloat()) it.copy(percent = ((it.amount / highestAmount)).toFloat())
} }