You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose排序列表问题:列表项消失后重现

问题:Jetpack Compose LazyColumn列表项移动时底部项消失后重现

我实现了一个按distance字段排序的LazyColumn列表,当某个列表项的distance更新导致位置变动时,列表底部的若干项会短暂消失,经过一两次更新后又重新出现。

我的排序逻辑是:先移除目标列表项,再通过前后线性搜索找到新位置插入,因为我认为项的移动范围不会超过一两行。

更新操作仅在单个线程执行,但我不确定Composable是否线程安全?或者组合列表时是否需要同步操作?


相关代码实现

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())
        }
    }
}

问题原因与解决方案

核心问题分析

  1. 非主线程更新状态容器:MutableStateList是Compose的状态容器,仅支持主线程修改。你在后台线程直接调用ViewModel方法修改列表,会导致状态更新与UI重组不同步,引发项消失、重复等异常。
  2. 重排序时索引计算错误:删除项后继续使用原index变量搜索新位置,此时列表长度已减少1,索引偏移会导致插入位置错误,进而干扰UI渲染逻辑。

修复方案

1. 强制在主线程更新状态

使用viewModelScope(默认绑定主线程)执行所有状态修改操作,避免后台线程直接操作MutableStateList。

2. 修复重排序的索引逻辑

删除项后,重新初始化搜索起始索引,基于更新后的列表计算插入位置。

修改后的代码

ViewModel代码

class TodoViewModel : ViewModel() {
    private val _todos = emptyList<Todo>().toMutableStateList()
    val todos: List<Todo>
        get() = _todos

    fun changeTitle(id: Int, title: String) {
        val index = _todos.indexOfFirst { it.id == id }
        if (index == -1) return
        _todos[index] = _todos[index].copy(title = title)
    }

    fun delete(index: Int) {
        if (index in _todos.indices) {
            _todos.removeAt(index)
        }
    }

    fun changeDistance(id: Int, delta: Int) {
        val index = _todos.indexOfFirst { it.id == id }
        if (index == -1) return

        val oldTodo = _todos[index]
        val newDistance = oldTodo.distance + delta
        val newTodo = oldTodo.copy(distance = newDistance)

        // 检查是否无需移动位置
        val needMove = !(
            (index == 0 || newDistance >= _todos[index-1].distance) &&
            (index == _todos.lastIndex || newDistance <= _todos[index+1].distance)
        )

        if (!needMove) {
            _todos[index] = newTodo
            return
        }

        // 删除旧项后,基于新列表重新计算插入位置
        _todos.removeAt(index)
        var newIndex = 0
        while (newIndex < _todos.size && newDistance > _todos[newIndex].distance) {
            newIndex++
        }

        _todos.add(newIndex, newTodo)
    }

    fun insert(todo: Todo) {
        val existingIndex = _todos.indexOfFirst { it.id == todo.id }
        if (existingIndex != -1) {
            _todos[existingIndex] = todo
            return
        }

        val newIndex = _todos.binarySearch { it.distance - todo.distance }.let {
            if (it < 0) -(it + 1) else it
        }
        _todos.add(newIndex, todo)
    }
}

Main Activity代码

class MainActivity : ComponentActivity() {
    private val todoViewModel by viewModels<TodoViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                TodoScreen(todoViewModel = todoViewModel)
            }
        }

        var nextId = 0
        // 使用viewModelScope确保状态更新在主线程执行
        todoViewModel.viewModelScope.launch {
            for (i in 0..10) {
                todoViewModel.insert(Todo(nextId, "new $nextId", Random.nextInt(25, 50)))
                nextId++
                delay(100) // 避免批量插入过快导致UI卡顿
            }

            for (i in 0..100) {
                delay(500)
                val todos = todoViewModel.todos
                if (todos.isEmpty()) continue
                val index = Random.nextInt(0, todos.size)
                val id = todos[index].id
                todoViewModel.changeDistance(id, Random.nextInt(-10, 10))
            }
        }
    }
}

@Composable
fun TodoScreen(
    modifier: Modifier = Modifier,
    todoViewModel: TodoViewModel
) {
    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),
        contentPadding = PaddingValues(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(defaultElevation = 8.dp)
    ) {
        Row(modifier = Modifier.padding(8.dp)) {
            Text(text = todo.title)
            Spacer(modifier = Modifier.weight(1f))
            Text(text = todo.distance.toString())
        }
    }
}

data class Todo(val id: Int, val title: String, val distance: Int)

内容的提问来源于stack exchange,提问作者user998303

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 20:42:41