5

So I'm updating my RecylerView with StateFlow<List> like following:

My data class:

data class Student(val name: String, var isSelected: Boolean)

My ViewModel logic:

fun updateStudentsOnSelectionChanged(targetStudent: Student) {
    val targetIndex = _students.value.indexOf(targetStudent)
    val isSelected = !targetStudent.isSelected

    _students.value[targetIndex].isSelected = isSelected        //<- doesn't work
} 

Problem: The UI is not changed, but the isSelected inside _student is changed, what's going on? (same to LiveData)

Sam Chen
  • 7,597
  • 2
  • 40
  • 73
  • 2
    It’s difficult to get proper behavior out of StateFlow or RecyclerView if you are using mutable classes or collections, because they have no way to detect changes between old and new when old and new are the same instance. – Tenfour04 Feb 19 '22 at 18:16
  • You should check updating function in fragment/activity. It may not call. If it call to check a new value. – Alexander Feb 19 '22 at 19:02

2 Answers2

1

I assume _students is a StateFlow<List>. Changing the isSelected property of the Student model doesn't trigger the StateFlow. The workaround would be to make the isSelected property of the Student data class immutable to get it compared when new state is set, create a MutableList out of the current list and copy the existing Student object with the new value for isSelected property:

data class Student(val name: String, val isSelected: Boolean)

val students = _students.value.toMutableList()
students[targetIndex] = students[targetIndex].copy(isSelected = isSelected)
_students.value = students
Sergio
  • 27,326
  • 8
  • 128
  • 149
  • This won’t work because StateFlow doesn’t emit duplicate consecutive items. Since you’re passing the same list instance back to it, it will see it as the same item. – Tenfour04 Feb 19 '22 at 18:14
  • Thanks for noticing @Tenfour04, you are right, I was relying that changing the `var isSelected: Boolean` will cause the flow to trigger, but it didn't happen I think because `isSelected` property is a mutable and it doesn't get compared in `equals` method. I've added a workaround. – Sergio Feb 19 '22 at 18:44
  • 1
    The mutable property does get compared, but it’s being compared to itself since the same instance is being pointed at. Both the list and the item in the list have to be changed to new instances for the comparison to detect a change (as your code does now), because otherwise the old and new values are the same instances. – Tenfour04 Feb 19 '22 at 21:41
1

Ok, thanks to @Tenfour04 and @Sergey, I finally found out that StateFlow/LiveData cannot detect the internal changes, that's because they are both actually comparing the Reference of the .value.

That means, If I want to force the StateFow<List> to update, the only way is to assign a new List to it, therefore I created the following helper extension function:

fun <T> List<T>.mapButReplace(targetItem: T, newItem: T) = map {
    if (it == targetItem) {
        newItem
    } else {
        it
    }
}

//this function returns a new List with the content I wanted

In Fragment:

val itemCheckAction: (Student) -> Unit = { student ->
    val newStudent = student.copy(isSelected = !student.isSelected)    //prepare a new Student

    viewModel.updateStudentsOnSelectionChanged(student, newStudent)    //update the StateFlow
}

In ViewModel:

fun updateStudentsOnSelectionChanged(currentStudent: Student, newStudent: Student) {
    val newList = _students.value.mapButReplace(currentStudent, newStudent)

    _students.value = newList       //assign a new List with different reference
}
Sam Chen
  • 7,597
  • 2
  • 40
  • 73