I'm displaying a list of items that I want to display in "distance" order. However, sometimes when an item's distance is updated causing it to move to a different place in the list, several items disappear from the bottom of the list. Another update or two later, they reappear again.
My strategy for sorting is to remove the item, linear search forwards or backwards in the list for its new location, and insert it, because I don't expect items to move more than one or two rows at a time.
I only have one thread updating the list. But are Composables thread safe? Or do I need to synchronize the list while it is being composed?
My ViewModel
class TodoViewModel : ViewModel() {
private val _todos = emptyList<Todo>().toMutableStateList()
val todos: List<Todo>
get() = _todos
fun changeTitle(id: Int, title: String) {
var index = _todos.indexOfFirst { todo -> todo.id == id }
if (index == -1)
return
_todos[index] = _todos[index].copy(title = title)
}
fun delete(index: Int) {
_todos.removeAt(index)
}
fun changeDistance(id: Int, delta: Int) {
var index = _todos.indexOfFirst { todo -> todo.id == id }
if (index == -1)
return
val distance = _todos[index].distance + delta
val oldDistance = _todos[index].distance
Log.i("xx", "Update $id at $index from $oldDistance to $distance")
val newTodo = _todos[index].copy(distance = distance)
if ((index == 0 || distance >= _todos[index-1].distance) &&
(index == _todos.size-1 || distance <= _todos[index+1].distance)) {
_todos[index] = newTodo
return
}
Log.i("xx", "Move $id from $index")
delete(index)
while (index > 0 && distance < todos[index-1].distance) {
index--
}
while (index < todos.size && distance > todos[index].distance) {
index++
}
Log.i("xx", "Move $id to $index")
_todos.add(index, newTodo)
}
fun insert(todo: Todo) {
val index = todos.indexOfFirst { t -> t.id == todo.id }
if (index != -1) {
_todos[index] = todo
return
}
var newIndex = todos.binarySearch {t -> t.distance - todo.distance }
if (newIndex < 0) newIndex = (-newIndex - 1)
_todos.add(newIndex, todo)
}
}
Main Activity
private val todoViewModel = TodoViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
TodoScreen(todoViewModel = todoViewModel)
}
}
var nextId = 0
val thread = Thread {
for (i in 0..10) {
todoViewModel.insert(Todo(nextId, "new $nextId", Random.nextInt(25, 50)))
nextId++
}
for (i in 0..100) {
Thread.sleep(500)
val functionNum = Random.nextInt(0, 100)
val index = Random.nextInt(0, todoViewModel.todos.size)
val id = todoViewModel.todos[index].id
todoViewModel.changeDistance(id, Random.nextInt(-10, 10))
}
}
thread.start()
}
}
@Composable
fun TodoScreen(
modifier: Modifier = Modifier,
todoViewModel: TodoViewModel = viewModel()
) {
Surface(color = MaterialTheme.colorScheme.background) {
TodoList(
modifier = modifier.fillMaxSize(),
list = todoViewModel.todos
)
}
}
@Composable
fun TodoList(
list: List<Todo>,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(items = list, key = { todo -> todo.id }) { todo ->
TodoItem(todo = todo, modifier = Modifier.fillMaxWidth() )
}
}
}
@Composable
fun TodoItem(todo: Todo,
modifier: Modifier
) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(8.dp, 8.dp, 8.dp, 8.dp, 8.dp, 8.dp)
) {
Row(modifier = Modifier.padding(8.dp)) {
Text(text = todo.title)
Spacer(modifier = Modifier.weight(1f))
Text(text = todo.distance.toString())
}
}
}