Search code examples
androidkotlinkotlin-coroutineskotlin-flowkotlin-stateflow

StateFlow Doesn't Emit on Field Changes


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?


Solution

  • 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.