6

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.

Indiana
  • 683
  • 7
  • 18
  • https://stackoverflow.com/questions/58171073/request-in-another-request-called-several-times-with-rxjava-and-retrofit/58171386#58171386 I have given answer to a similar problems. Please check and let me know if it helps you – Kishan Maurya Oct 11 '19 at 13:29
  • You can use `SingleLiveEvent` as shown here: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 – Saurabh Thorat Oct 11 '19 at 13:30
  • Because `LiveData` works like a `BehaviorSubject`. `onChanged` is invoked when you subscribe. You can use the hack-around linked above (`SingleLiveEvent`). – EpicPandaForce Oct 11 '19 at 14:19
  • You can also look at [this sample](https://github.com/Zhuinden/event-emitter/blob/9f7ea15291e74241cd9f41d91b7af16dcbaa0db1/event-emitter-sample/src/main/java/com/zhuinden/eventemittersample/features/words/WordListFragment.kt#L54-L58) – EpicPandaForce Oct 11 '19 at 14:25
  • any idea how to solve this problem? all the listed suggestion no working to me – user2301281 Dec 24 '19 at 06:13
  • I used the concept of a SingleEvent and a SingleEventObserver to handle this issue. As mentioned by @SaurabhThorat - https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 – Indiana Dec 28 '19 at 16:23
  • Any one can help for same issue in JAVA. – Davinder Goel Jun 17 '20 at 17:30

3 Answers3

11

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

Jose
  • 2,479
  • 1
  • 20
  • 17
  • Looks like, I ended up wrapping every LiveData object into Event. Is it good design practice? – AndroidDev Jul 05 '20 at 09:39
  • This's been recommended by Android team themselves to tackle a situation where we need consume update only once. See link given. But I wouldn't advice wrap every live data. Use if it's required only. – Jose Jul 05 '20 at 14:18
  • i tried your code and it works fine but it didn't solve my problem , live is triggered everytme i move back to fragment , anyone has this solution working for him ? – Taki Sep 24 '20 at 00:31
  • Thank you so much for this information! Very good to know! – Nathan Donaldson Mar 09 '21 at 08:16
  • @takieddine its solve problem triggered when u go back to Fragmnet!) – Fortran Jul 01 '21 at 16:19
0

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.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
0

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.

EpicJoker
  • 349
  • 2
  • 15