Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 543
Indiana Avatar asked Oct 11 '19 13:10

Indiana


1 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

like image 58
Jose Avatar answered Nov 15 '22 10:11

Jose