Why is onChanged being called when I navigate back to a fragment?

Viewed 4271

I have a ViewModel handling my business logic and I am using Koin to inject this into my activity and each of my fragments. However after I navigate from Fragment A - Fragment B and navigate back to Fragment A, my observer is triggered again. Why is this happening and how do I stop this onChanged being triggered when I navigate back?

I have tried setting both 'this' and 'viewLifecycleOwner' as the LifecycleOwner of the LiveData.

I have also tried moving the observable to onCreate, onActivityCreated and onViewCreated

My ViewModel:

class MyViewModel : ViewModel() {

    private val _myData = MutableLiveData<Data>()
    val myData = LiveData<Data>()
        get() = _myData

    fun doSomething() {
        ... // some code
        _myData.postValue(myResult)
}

MyActivity:

class Activity : BaseActivity() {

    private val viewModel by viewModel<MyViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        setSupportActionBar(main_toolbar)

        subscribeUI()
    }

    private fun subscribeUI() {
        myViewModel.isLoading.observe(this, Observer {
            toggleProgress(it)
        })
    }
}

Fragment A:

class FragmentA : BaseFragment() {

    private val viewModel by sharedViewModel<MyViewModel>()

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        subscribeUI()
    }

    private fun subscribeUI() {
        viewModel.myData.observe(viewLifecycleOwner, Observer {
            val direction =
                FragmentADirections.actionAtoB()
            mainNavController?.navigate(direction)
        })
    }
}

Fragment B:

class FragmentB : BaseFragment() {

    private val authViewModel by sharedViewModel<LoginViewModel>()

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        subscribeUI()
    }

    private fun subscribeUI() {
        viewModel.otherData.observe(viewLifecycleOwner, Observer {
            // Do something else...
        })
    }
}

When I navigate from Fragment A -> Fragment B, everything works as I expect. However when I navigate back to Fragment A from Fragment B (by pressing the back button) the onChanged method from the Observer on myData is called and the navigation moves back to Navigation B.

3 Answers

This is an expected behaviour when using MutableLiveData. I think your problem has nothing to do as to where to add or remove subscribers.

MutableLiveData holds last value it is set with. When we go back to previous fragment, our LiveData observes are notified again with existing values. This is to preserve the state of your fragment and that's the exact purpose of LiveData.

Google itself has addressed this and provided a way to override this behaviour, which is using an Event wrapper.

  1. Create a wrapper to wrap an event.
  2. Create an observer to observe for this wrapped events.

Let's see code in action

  1. Create wrapper event
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {

 var hasBeenHandled = false
     private set // Allow external read but not write

 /**
  * Returns the content and prevents its use again.
  */
 fun getContentIfNotHandled(): T? {
     return if (hasBeenHandled) {
         null
     } else {
         hasBeenHandled = true
         content
     }
 }

 /**
  * Returns the content, even if it's already been handled.
  */
 fun peekContent(): T = content
}
  1. Create observer
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
 override fun onChanged(event: Event<T>?) {
     event?.getContentIfNotHandled()?.let { value ->
         onEventUnhandledContent(value)
     }
 }
}
  1. Declare live data object in View model?
// Declare live data object
val testLiveData: MutableLiveData<Event<Boolean>
             by lazy{ MutableLiveData<Event<Boolean>>() }
  1. Set data for live data object
testLiveData.postValue(Event(true))
  1. Observe for this live data in fragment
viewModel?.testLiveData?.observe(this, EventObserver { result ->
 // Your actions
}

Detailed reference: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

This is expected because you are resubscribing to a LiveData, and LiveData replays its last emitted value when you subscribe to it.

You should use a different component that doesn't replay the last emitted value on resubscription, for example you could use https://github.com/Zhuinden/live-event instead.

I ended up doing something a little less complicated than trying to change to wrapped events.

Bare in mind that my solution is completely in my situation and although its not the recommended way, we used this a solution in order to provide quick results for what we wanted.

My problem was the same, when I navigated back the LiveData (i am using Flow) was retriggering the same value as when it left the fragment. This was a problem because it was triggering the observer which then read the status of the value and if the status was set to COMPLETE, we would navigate away to the next page.

This will work of course if you have something similar where you observe some kind of information from the source to make a decision.

Anyway, the solution for me was on navigate to then set the source data to the default which would be the object with status NONE.

This stopped the observer from triggering the last state when being navigated to from the back button.

I created the function within the viewModel

fun clearCustomerSearch() {
    customerSearchStateFlow.value = HttpResponse.none()
}

this would then trigger the observer, however it would do nothing because the state is NONE.

Hopefully this will helps someone if they are not wanting to use event wrapping, although for a long term and more complete solution, I would look at Jose's answer.

Related