The problem is you've set up that cycle, where pushing state to the UI (e.g. observing changes in the LiveData
) causes an event to be pushed to the data layer (the TextWatcher
triggering when the EditText
is changed).
Really those events should be in response to something the user does - displaying an updated UI state shouldn't trigger any non-UI actions, it should just be about reacting to new state and displaying it, job done.
So fundamentally, your TextWatcher
is a problem, because it doesn't know the difference between a change caused by user interaction (which you want to push to the data layer) and a simple display update (which shouldn't trigger any reactive behaviour). And because of the way you have it set up, even a user interaction will cause this eventually:
user edit -> watcher -> VM update -> observer updates EditText -> watcher -> VM update...
This makes it tricky to implement the usual approaches to avoiding cyclic TextWatcher
behaviour, where you make afterTextChanged
set a flag or something before it makes a second change - that way, when it gets called again in response to its own change, it can avoid doing it again by checking the flag (and then resetting it for next time). But that's not really possible here, because it's not causing the extra updates itself - an external change is happening, and it doesn't know what's causing it.
So you'll probably want a different way of breaking that cycle. One way is to just not update the EditText
if its contents haven't changed:
createMedicineViewModel.formData.launchAndCollectIn(viewLifecycleOwner) {
if (binding.edtA.text != it.editTextA) {
binding.edtA.text = it.editTextA
}
}
You could always write a function for that, to make it a little less unwieldy.
fun TextView.setTextIfChanged(newText: String) {
if (text != newText) text = newText
}
binding.edtA.setTextIfChanged(it.editTextA)
And now, if the contents are changed, it'll push an update to the VM, which pushes an update to observers containing those same contents. Because there's no actual change to make now, you skip it and the loop is broken.
That might seem a little clumsy though, having to wire things up to take care of that specific behaviour. A more general approach is calling distinctUntilChanged()
on the StateFlow
:
private val _formData = MutableStateFlow(FormData()).distinctUntilChanged()
Now, as soon as you set a FormData
on that LiveData
which is equal to the current value, it'll skip emitting an update to observers, breaking the cycle. Because in your example you have a data class using String
s, you get that equality check for free, and eventually things will settle to the point where your TextWatcher
pushes an "update" that doesn't change anything.
But I said "eventually things will settle" because, depending on how you have things set up, it's possible you push a state that updates multiple EditText
s, and each one of those will trigger their own cycle of pushing updates. So you have to be aware of that, and decide whether a single update going round and round the loop a few times is ok, or if you want to avoid that.
The other approach you can take (and this is probably easiest for your situation) is some kind of general "I'm updating" flag. Anything that's updating the UI (e.g. observers, initial setup code) can set some updating
flag before making the changes, and unset it when finished.
Your TextWatcher
s can check that flag, and ignore any changes that happen while it's set. That way, your "displaying state" changes don't push anything to the VM, and user changes will only happen while the flag is unset, causing update events to be pushed.
The tricky part there is ensuring you set that flag in all the situations you need to, and unset it at the end - might be worth creating a displayState(formData: FormData)
function that everything (including setup code) uses to display a particular state in the UI, then it's all handled in one place.