Home > database >  Unable to trigger compose re-composition after re-assigning mutableStateList inside a coroutine scop
Unable to trigger compose re-composition after re-assigning mutableStateList inside a coroutine scop

Time:05-27

I'm still a bit of a beginner with Jetpack compose and understanding how re-composition works. So I have a piece of code calls below inside a ViewModel.

SnapshotStateList

var mutableStateTodoList = mutableStateListOf<TodoModel>()
    private set

during construction of the view model, I execute a room database call

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

then I have an action from a the ui that triggers adding of a new Todo to the list and expecting a re-composition from the ui that shows a card composable

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

I can't figure out why it doesn't trigger re-composition.

However if I modify the init code block below, and invoke the onFabClick() action, it triggers re-composition

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

or this, taking out the re-assigning of the mutableStateList outside of the coroutine scope also works (triggers re-composition).

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

Not quite sure where the problem if it is within the context of coroutine or SnapshotStateList itself.

Everything is also working as expected when the code was implemented this way below, using standard list inside a wrapper and performing a copy (creating new reference) and re-assigning the list inside the wrapper.

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

Same init{...} call

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

To summarize, inside a coroutine scope, why this works

// mutableStateList
todoList.addAll(it)

while this one does not?

 // mutableStateList
 todoList = it.toMutableStateList()

also why does ordinary list inside a wrapper and doing copy() works?

CodePudding user response:

The mutable state in Compose can only keep track of updates to the containing value. Here is simplified code on how MutableState could be implemented:

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

When the state is used by some view, this view subscribes to updates of this particular state.

So, if you declare the property as follows:

var state = MutableState(1)

and try to update it with state = 2.toMutableState() (this is analogous to your mutableStateTodoList = listTypeTodo.toMutableStateList()), triggerRecomposition cannot be called because you create a new object which resets all the listeners. Instead, to trigger recomposition you should update it with state.value = 2.

With mutableStateList, the analog of updating value is any method of MutableList interface that updates containing list, including addAll.

Inside init it works because no view is subscribed to this state so far, and that's the only place where methods such as toMutableStateList should be used.

It is important to always define mutable states as immutable property with val in order to prevent such mistakes. To make it mutable only from view model, you can define it like this, and make updates on _mutableStateTodoList:

private val _mutableStateTodoList = mutableStateListOf<TodoModel>()
val mutableStateTodoList: List<TodoModel> = _mutableStateTodoList

The only exception when you can use var is using mutableStateOf with delegation - this is where you can use it with private set because in that case the delegation does the work for you by not modifying the container, but only it's value property. Such method cannot be applied to mutableStateListOf, because there's no single value field that's responsible for the data in case of list.

var someValue by mutableStateOf(1)
    private set
  • Related