In my View
composable I have a list of items in a LazyColumn and a "Sort" button that toggles between ascending and descending sort order. The list comes from my state holder class ViewComponent
. This works fine.
I also have a "Search" button that retrieves data from a database that is then passed to View
to replace the displayed data.
Although the data is replaced, the sort order is now broken: Whenever I press the "Sort" button the list is sorted and then instantly reverted back. I verified with println
that the sort is actually executed as intended, but as soon as that finishes another recomposition seems to happen that resets the sort.
Is it possible that the function injectListFromDatabase
is responsible for it? It only replaces the data class with the values from the database.
For clarity's sake I reduced my data classes to a bare minimum, but they actually have much more properties. The LazyColumn displays a list of GetToolsWithService
objects:
data class GetToolsWithService(
val id: Int,
)
data class ToolListState(
val tools: List<GetToolsWithService>,
)
This is my View
with the LazyColumn:
@Composable
fun View(
component: ViewComponent = ViewComponent(),
listFromDatabase: List<GetToolsWithService>,
) {
if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)
val stateList by component.stateList
Button(onClick = {
component.sort()
}) {
Text("Sort")
}
LazyColumn {
items(stateList.tools) { Text("$it") }
}
}
The stateholder:
class ViewComponent(
private val sortTable: SortTable = SortTable(),
initialListOfTools: List<GetToolsWithService> = List(10) { GetToolsWithService(it) },
) {
val stateList: MutableState<ToolListState> = mutableStateOf(ToolListState(initialListOfTools))
fun sort() {
println("Sort Start")
stateList.value = stateList.value.copy(tools = sortTable(stateList.value.tools))
stateList.value.tools.forEach { println(it) }
println("Sort end")
}
fun injectListFromDatabase(list: List<GetToolsWithService>) {
stateList.value = stateList.value.copy(tools = list)
}
}
And this is used for sorting:
enum class SortOrder {
ASCENDING, DESCENDING
}
class SortTable(
private var sortingOrderData: SortOrder = SortOrder.ASCENDING,
) {
operator fun invoke(list: List<GetToolsWithService>): List<GetToolsWithService> =
if (sortingOrderData == SortOrder.DESCENDING) {
sortingOrderData = SortOrder.ASCENDING
list.sortedBy { it.id }
} else {
sortingOrderData = SortOrder.DESCENDING
list.sortedByDescending { it.id }
}
}
The following is only used to emulate how the data from the database is passed to the View
:
Column {
var list: List<GetToolsWithService> by remember { mutableStateOf(emptyList()) }
Button(onClick = {
list = List(10) { GetToolsWithService(it + 100) } // "+100" so the items can be more easily distinguished
}) { Text("Search") }
View(
listFromDatabase = list,
)
}
The problem lies indeed with how you call injectListFromDatabase
. On every (re-)composition this line of code is executed:
if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)
That doesn't hurt as long as the list is empty, but when "Search" was pressed this is not only executed once, it is repeated on every recomposition.
Let's take a look at what that means when "Sort" is pressed after "Search":
component.sort()
is called and changes the stateList
so it is now properly sorted. You already verified that by your println
statements.stateList
triggers a recomposition so View
is executed again.injectListFromDatabase
is called. This replaces the content of stateList
with listFromDatabase
. Now, this means that the list we just sorted in 1. is never displayed, it is just thrown away and listFromDatabase
is used instead.The problem is that injectListFromDatabase
is not only called when listFromDatabase
changes, it is also called on every other recomposition.
The fix is simple:
LaunchedEffect(listFromDatabase) {
if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)
}
This wraps injectListFromDatabase
in a lambda that is only executed when the LaunchedEffect
's parameters change. I supplied listFromDatabase
as the parameter, so this effectively skips the execution of injectListFromDatabase
for each recomposition where listFromDatabase
didn't change - as it is the case when the "Sort" button is pressed.
Sorting works now also after the "Search" button was pressed.