在协程范围内重新分配 mutableStateList 后无法触发组合重新组合

Unable to trigger compose re-composition after re-assigning mutableStateList inside a coroutine scope

我对 Jetpack compose 和了解重新组合的工作原理仍然有点陌生。 所以我在 ViewModel.

下面有一段代码调用

SnapshotStateList

var mutableStateTodoList = mutableStateListOf<TodoModel>()
    private set

在构建视图模型期间,我执行了一个房间数据库调用

init {
    viewModelScope.launch {
        fetchTodoUseCase.execute()
            .collect { listTypeTodo ->
                mutableStateTodoList = listTypeTodo.toMutableStateList()
            }
    }
}
   

然后我有一个来自 ui 的动作,它会触发向列表添加一个新的待办事项,并期待 ui 的重新组合显示一张可组合的卡片

fun onFabClick() {
    todoList.add(TodoModel())
}

我不明白为什么它不触发重新合成。

但是,如果我修改下面的初始化代码块,并调用 onFabClick() 操作,它会触发重新组合

init {
    viewModelScope.launch {
        fetchTodoUseCase.execute()
            .collect { listTypeTodo ->
                mutableStateTodoList.addAll(listTypeTodo)
            }
    }
}

或者这样,在协程作用域之外取出 mutableStateList 的重新分配也有效(触发重新组合)。

init {
    // just trying to test a re-assigning of the mutableStateList property
    mutableStateTodoList = emptyList<TodoModel>().toMutableStateList()
}

不确定ui问题出在协程上下文或SnapshotStateList本身。

当代码以下面的方式实现时,一切也都按预期工作,在包装器内使用标准列表并执行复制(创建新引用)并在包装器内重新分配列表。

var todoStateWrapper by mutableStateOf<TodoStateWrapper>(TodoStateWrapper)
    private set

相同的初始化{...}调用

init {
    viewModelScope.launch {
        fetchTodoUseCase.execute()
            .collect { listTypeTodo ->
                todoStateWrapper = todoStateWrapper.copy (
                    todoList = listTypeTodo
                )
            }
    }
}

总而言之,在协程范围内,为什么这样行得通

// mutableStateList
todoList.addAll(it)

而这个没有?

 // mutableStateList
 todoList = it.toMutableStateList()

为什么普通列表在包装器中并执行 copy() 工作?

Compose 中的可变状态只能跟踪包含值的更新。下面是关于如何实现 MutableState 的简化代码:

class MutableState<T>(initial: T) {
    private var _value: T = initial
    
    private var listeners = MutableList<Listener>

    var value: T
        get() = _value
        set(value) {
            if (value != _value) {
                _value = value
                listeners.forEach {
                    it.triggerRecomposition()
                }
            }
        }
    
    fun addListener(listener: Listener) {
        listeners.add(listener)
    }
}

当某个视图使用该状态时,该视图会订阅该特定状态的更新。

因此,如果您声明 属性 如下:

var state = MutableState(1)

并尝试用 state = 2.toMutableState() 更新它(这类似于您的 mutableStateTodoList = listTypeTodo.toMutableStateList()),无法调用 triggerRecomposition,因为您创建了一个重置​​所有侦听器的新对象。相反,要触发重组,您应该使用 state.value = 2.

更新它

mutableStateList一样,更新值类比是MutableList接口更新包含列表的任意方法,包括addAll.

init 内部它可以工作,因为到目前为止没有视图订阅此状态,并且这是唯一应该使用 toMutableStateList 等方法的地方。

重要的是始终将可变状态定义为不可变 属性 和 val 以防止此类错误。要使其仅在视图模型中可变,您可以这样定义它,并在 _mutableStateTodoList:

上进行更新
private val _mutableStateTodoList = mutableStateListOf<TodoModel>()
val mutableStateTodoList: List<TodoModel> = _mutableStateTodoList

您可以使用 var 的唯一例外是使用 mutableStateOf 和委托 - 这是您可以将它与 private set 一起使用的地方,因为在这种情况下委托会为你不修改容器,而只是它 value 属性。这种方法不能应用于 mutableStateListOf,因为在列表的情况下没有单个 value 字段负责数据。

var someValue by mutableStateOf(1)
    private set