I have a data class:
data class Student(
val name: String,
var isSelected: Boolean = false
)
And in the ViewModel
:
class FirstViewModel : ViewModel() {
private val _student = MutableStateFlow(Student("Sam"))
val student = _student.asStateFlow()
fun updateStudentOnSelectionChange(student: Student) {
val newStudent = student.copy().apply {
isSelected = !isSelected
}
_student.update { newStudent }
}
}
I observe changes in Fragment
:
class FirstFragment : Fragment() {
private lateinit var binding: FragmentFirstBinding
private lateinit var viewModel: FirstViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_first, container, false)
viewModel = ViewModelProvider(this)[FirstViewModel::class.java]
binding.updateButton.setOnClickListener {
viewModel.updateStudentOnSelectionChange(viewModel.student.value)
}
viewModel.student.collectOnStarted(viewLifecycleOwner) {
Log.v("xxx", "isSelected: ${it.isSelected}")
}
return binding.root
}
}
So far it works fine, I can see the changes log, but if I move the isSelected
out of the constructor like this:
data class Student(
val name: String
) {
var isSelected: Boolean = false
}
The StateFlow
doesn't emit any more, why?
StateFlow doesn’t emit if the new value equals the old value. If your property isn’t in the constructor of your data class, then it doesn’t participate in the default implementation of equals()
for the class, so it doesn’t affect whether two instances are considered equal.
A suggestion for robustness: keep isSelected
in the constructor and make it a val
. Change the content of your updating function to just
_student.update { student.copy(isSelected = !student.isSelected) }
Then you cannot accidentally mutate an obsolete instance or change an instance while it is still part of what the flow is using to compare old and new values. IMO, it is a code smell for a class with any mutable properties to be the type of a StateFlow or LiveData.