Viewmodel Onchange Gets Called Multiple Times When Back from Fragment

ViewModel onchange gets called multiple times when back from Fragment

The problem here is that when you dettach the fragment from the acitivity, both fragment and its viewmodel are not destroyed. When you come back, you add a new observer to the livedata when the old observer is still there in the same fragment (If you add the observer in onCreateView()).
There is an article (Even a SO thread in fact) talking about it (with solution).

The easy way to fix it (also in the article) is that remove any observer from the livedata before you add observer to it.

Update:
In the support lib v28, a new LifeCycleOwner called ViewLifeCycleOwner should fix that more info in here

Kotlin ViewModel onchange gets called multiple times when back from Fragment (using Lifecycle implementation)

That's how LiveData works, it's a value holder, it holds the last value.

If you need to have your objects consumed, so that the action only triggers once, consider wrapping your object in a Consumable, like this

class ConsumableValue<T>(private val data: T) {

private val consumed = AtomicBoolean(false)

fun consume(block: ConsumableValue<T>.(T) -> Unit) {
if (!consumed.getAndSet(true)) {
block(data)
}
}
}

then you define you LiveData as

val action: LiveData<ConsumableValue<Action>>
get() = mutableAction
private val mutableAction = MutableLiveData<ConsumableValue<Action>>()

then in your observer, you'd do

private val observer = Observer<ConsumableValue<DashboardUserViewModel.Action>> {
it?.consume { action ->
when (action) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}

Why onChange() called twice when fragment recreated

I SOLVED THIS!!!
When I creating my ViewModel class, I pass "this" 1-st parameter into method

buyViewModel = ViewModelProviders.of(**this**, viewModelFactory).get(BuyViewModel.class);

But I need pass "getActivity()", and code looks like that

buyViewModel = ViewModelProviders.of(**getActivity()**,viewModelFactory).get(BuyViewModel.class);

Flow onEach/collect gets called multiple times when back from Fragment

Reason

It happens because of tricky Fragment lifecycle. When you come back from Fragment B to Fragment A, then Fragment A gets reattached. As a result fragment's onViewCreated gets called second time and you observe the same instance of Flow second time. Other words, now you have one Flow with two observers, and when the flow emits data, then two of them are called.

Solution 1 for Fragment

Use viewLifecycleOwner in Fragment's onViewCreated. To be more specific use viewLifecycleOwner.lifecycleScope.launch instead of lifecycleScope.launch. Like this:

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
availableLanguagesFlow.collect {
languagesAdapter.setItems(it.allItems, it.selectedItem)
}
}

Solution 2 for Activity

In Activity you can simply collect data in onCreate.

lifecycleScope.launchWhenStarted {
availableLanguagesFlow.collect {
languagesAdapter.setItems(it.allItems, it.selectedItem)
}
}

Additional info

  1. Same happens for LiveData. See the post here. Also check this article.
  2. Make code cleaner with Kotlin extension:

extension:

fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycleScope.launchWhenStarted {
this@launchWhenStarted.collect()
}
}

in fragment onViewCreated:

availableLanguagesFlow
.onEach {
//update view
}.launchWhenStarted(viewLifecycleOwner)

Update

I'd rather use now repeatOnLifecycle, because it cancels the ongoing coroutine when the lifecycle falls below the state (onStop in my case). While without repeatOnLifecycle, the collection will be suspended when onStop. Check out this article.

fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner)= with(lifecycleOwner) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
try {
this@launchWhenStarted.collect()
}catch (t: Throwable){
loge(t)
}
}
}
}

Why LiveData observer is being triggered twice for a newly attached observer

I forked your project and tested it a bit. From all I can tell you discovered a serious bug.

To make the reproduction and the investigation easier, I edited your project a bit. You can find updated project here: https://github.com/techyourchance/live-data-problem . I also opened a pull request back to your repo.

To make sure that this doesn't go unnoticed, I also opened an issue in Google's issue tracker:

Steps to reproduce:

  1. Ensure that REPRODUCE_BUG is set to true in MainFragment
  2. Install the app
  3. Click on "add trashed note" button
  4. Switch to TrashFragment
  5. Note that there was just one notification form LiveData with correct value
  6. Switch to MainFragment
  7. Click on "add trashed note" button
  8. Switch to TrashFragment
  9. Note that there were two notifications from LiveData, the first one with incorrect value

Note that if you set REPRODUCE_BUG to false then the bug doesn't
reproduce. It demonstrates that subscription to LiveData in
MainFragment changed the behavior in TrashFragment.

Expected result: Just one notification with correct value in any case.
No change in behavior due to previous subscriptions.

More info: I looked at the sources a bit, and it looks like
notifications being triggered due to both LiveData activation and new
Observer subscription. Might be related to the way ComputableLiveData
offloads onActive() computation to Executor.

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

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

ViewModel observe is running multiple times for a single hit to API

Every time hitSecondChartApi() is called, a new observer is attached to LiveData and these duplicated observers are causing the unexpected result. Solutions like SingleEvent or SingleLiveData can technically hide this effect but it will not remove the root cause.

Correct solution is to call responseSecondChartData only once at the beginning of the Fragment/Activity initialization. For example:

fun onActivityCreated(...) {
userModel.responseSecondChartData.observe(this,
Observer {
Log.e(TAG, "Second chart data: " + it.toString())
Utils.debugger("FRAG ", "$it")
secondChartData = it!!
if (it.size > 0) {
splitSecondParentList(it!!)
} else
Utils.showLongToast(requireActivity(), "No Data for Distribution")
})
}

private fun hitSecondChartApi(country: String, category: String, market: String, weightKpi: String) {
userModel.getResponseShareSecondChartData(country, category, market, weightKpi)
}

There is no need to remove observer in above code because its creating only one observer.



Related Topics



Leave a reply



Submit