LiveData prevent receive the last value when start observing

57,485

Solution 1

I`m using this EventWraper class from Google Samples inside MutableLiveData

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
public class Event<T> {

    private T mContent;

    private boolean hasBeenHandled = false;


    public Event( T content) {
        if (content == null) {
            throw new IllegalArgumentException("null values in Event are not allowed.");
        }
        mContent = content;
    }
    
    @Nullable
    public T getContentIfNotHandled() {
        if (hasBeenHandled) {
            return null;
        } else {
            hasBeenHandled = true;
            return mContent;
        }
    }
    
    public boolean hasBeenHandled() {
        return hasBeenHandled;
    }
}

In ViewModel :

 /** expose Save LiveData Event */
 public void newSaveEvent() {
    saveEvent.setValue(new Event<>(true));
 }

 private final MutableLiveData<Event<Boolean>> saveEvent = new MutableLiveData<>();

 public LiveData<Event<Boolean>> onSaveEvent() {
    return saveEvent;
 }

In Activity/Fragment

mViewModel
    .onSaveEvent()
    .observe(
        getViewLifecycleOwner(),
        booleanEvent -> {
          if (booleanEvent != null)
            final Boolean shouldSave = booleanEvent.getContentIfNotHandled();
            if (shouldSave != null && shouldSave) saveData();
          }
        });

Solution 2

Faced the same problem, and I created some simple kotlin extention functions which can solve the problem easily.

Usage as below:

val liveData = MutableLiveData<String>()
liveData.value = "Hello"

val freshResult = mutableListOf<String>()
val normalResult = mutableListOf<String>()

liveData.observeForeverFreshly(Observer {
    freshResult.add(it)
})

liveData.observeForever(Observer {
    normalResult.add(it)
})

liveData.value = "World"

assertEquals(listOf("World"), freshResult)
assertEquals(listOf("Hello", "World"), normalResult)

Basic source code is explained as bllow.

For some more detail (to support some special situations for example MediatorLiveData returned from Transformations.map), you can view it in github : livedata-ext

FreshLiveData.kt

fun <T> LiveData<T>.observeFreshly(owner: LifecycleOwner, observer: Observer<in T>) { 
    // extention fuction to get LiveData's version, will explain in below.
    val sinceVersion = this.version()
    this.observe(owner, FreshObserver<T>(observer, this, sinceVersion))
}

fun <T> LiveData<T>.observeForeverFreshly(observer: Observer<in T>, skipPendingValue: Boolean = true) {
    val sinceVersion = this.version()
    this.observeForever(FreshObserver<T>(observer, this, sinceVersion))
}

// Removes the observer which has been previously observed by [observeFreshly] or [observeForeverFreshly].
fun <T> LiveData<T>.removeObserverFreshly(observer: Observer<in T>) {
    this.removeObserver(FreshObserver<T>(observer, this, 0))
}

class FreshObserver<T>(
    private val delegate: Observer<in T>,
    private val liveData: LiveData<*>,
    private val sinceVersion: Int
) : Observer<T> {

    override fun onChanged(t: T) {
        if (liveData.version() > sinceVersion) {
            delegate.onChanged(t)
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        if (delegate != (other as FreshObserver<*>).delegate) return false
        return true
    }

    override fun hashCode(): Int {
        return delegate.hashCode()
    }
}

Becasue we need to access LiveData's pcakage visibile methond getVersion() for comparasion, so create a class in package android.arch.lifecycle or androidx.lifecycle (AndroidX):

LiveDataHiddenApi.kt

package androidx.lifecycle

fun LiveData<*>.version(): Int {
    return this.getVersion()
}

Solution 3

Having some experience with RxJava, I've grown accustomed to thinking that such behavioral requirements are typically a concern of the Observeable (LiveData in our case). There are many operators such as replay(), that can control what's actually emitted (and when) compared to the actual publishes made by the user. In essence, SingleLiveEvent has the same notion to it, too.

I therefore came up with this modified implementation of MutableLiveData called VolatileLiveData:

open class VolatileLiveData<T> : MutableLiveData<T>() {
    private val lastValueSeq = AtomicInteger(0)
    private val wrappers = HashMap<Observer<in T>, Observer<T>>()

    @MainThread
    public override fun setValue(value: T) {
        lastValueSeq.incrementAndGet()
        super.setValue(value)
    }

    @MainThread
    public override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        val observerWrapper = ObserverWrapper(lastValueSeq, observer)
        wrappers[observer] = observerWrapper
        super.observe(owner, observerWrapper)
    }

    @MainThread
    public override fun observeForever(observer: Observer<in T>) {
        val observerWrapper = ObserverWrapper(lastValueSeq, observer)
        wrappers[observer] = observerWrapper
        super.observeForever(observerWrapper)
    }

    @MainThread
    public override fun removeObserver(observer: Observer<in T>) {
        val observerWrapper = wrappers[observer]
        observerWrapper?.let {
            wrappers.remove(observerWrapper)
            super.removeObserver(observerWrapper)
        }
    }
}

private class ObserverWrapper<T>(private var currentSeq: AtomicInteger, private val observer: Observer<in T>) : Observer<T> {
    private val initialSeq = currentSeq.get()
    private var _observer: Observer<in T> = Observer {
        if (currentSeq.get() != initialSeq) {
            // Optimization: this wrapper implementation is only needed in the beginning.
            // Once a valid call is made (i.e. with a different concurrent sequence), we
            // get rid of it any apply the real implementation as a direct callthrough.
            _observer = observer
            _observer.onChanged(it)
        }
    }

    override fun onChanged(value: T) {
        _observer.onChanged(value)
    }
}

First, similar to @emandt, I've associate unique sequences to each live value -- but strictly in the scope of the live data itself. This sequence is set whenever a value is set to the live data.

Second, inspired by SingleLiveData, I've introduced wrappers around the user's observer that only call through to it if the sequence is different (i.e. a new value has been set since subscription was made).

That basically sums it up, but for full documentation, please head over to my gist.

Usage

As for using it - if you have full control over the LiveData, simply use VolatileLiveData as you would use MutableLiveData. If the data originally comes from somewhere else (e.g. Room), Transformations.switchMap() can be used so as to make a 'switch' to the volatile implementation.

Solution 4

I don't think it is possible to prevent LiveData from receiving the last value when start observing if you are using them as it is. What you can do is to extend ViewModel class and make it notify the view only if the observer is added.

Another option is to simply ignore the callback.

  1. Add a flag to the ViewModel.

    private boolean isFirstTime = true;
    
    public boolean isFirstTime() { return isFirstTime; }
    
    public boolean onObserverAdded() { isFirstTime = false; }`
    
  2. Add checking in the callback

    @Override
    public void onChanged(@Nullable final String newName) {
    boolean ignore = ((MyViewModel)ViewModelProviders.of(MyActivity.this).get(MyViewModel.class)).isFirstTime();
    if(ignore) return;
    
    // Update the UI
    }
    
  3. Finally call onObserverAdded() after observer is added.

Solution 5

I created a new Class that will hold my real data and a "special ID":

class LiveDataItem {
    long mRealtimeNanos;
    YOUR_PREVIOUS_LIVEDATA_TYPE mData;
    LiveDataItem(YOUR_PREVIOUS_LIVEDATA_TYPE data, long realtimeNanos) {
        this.mRealtimeNanos = realtimeNanos;
        this.mData = data;
    }
}

Then I created a new "global" variable:

final List<Long> mExcludedRealtimeNanos = new ArrayList<>;

At this point I choose to "set/postValue()" of my "LiveDataItem" type instead of the original "YOUR_PREVIOUS_LIVEDATA_TYPE" type via a new and custom "postValue()" method:

public void myPostValue(YOUR_PREVIOUS_LIVEDATA_TYPE data, boolean notifyWhenObserved) {
    long cRealtimeNanos = SystemClock.realtimeNanos();
    if (!notifyWhenObserved) mExcludedRealtimeNanos.add(cRealtimeNanos);
    ....postValue(new LiveDataItem(data, cRealtimeNanos));
}

Then I created a normal Observer that will receive all "Changed()" events and inside it I put a check about "RealtimeNanos":

public void onChanged(LiveDataItem myDataItem) {
    boolean cFound = false;
    for (Long cRealtimeNanos : mExcludedRealtimeNanos) {
        if (cRealtimeNanos == myDataItem.mRealtimeNanos) {
            cFound = true;
            break;
        }
    }
    //check if it was found --> NO: it means that I wish to get the notification
    if (!cFound) mMyOnChangedCallback(myDataItem.mData)
}

Obliviously the "mMyOnChangedCallback()" method is a callback function that will be called whenever the original "onChanged()" event is raised BUT only if you set to notify it during data creation time.

You can choose to be notified again just by removing THAT RealtimeNanos from "mExcludedRealtimeNanos" and then attach a new Observer to that LiveData.

Few changes could improve this code but I wrote you what I remember of my old code (I'm currently away from my computer at this moment). For example we can decide to remove a value from "mExcludedRealtimeNanos" when a new data is posted using our custom postValue() method....

Share:
57,485

Related videos on Youtube

Pavel Poley
Author by

Pavel Poley

www.pavelpoley.com

Updated on March 23, 2022

Comments

  • Pavel Poley
    Pavel Poley over 2 years

    Is it possible to prevent LiveData receive the last value when start observing? I am considering to use LiveData as events.

    For example events like show message, a navigation event or a dialog trigger, similar to EventBus.

    The problem related to communication between ViewModel and fragment, Google gave us LiveData to update the view with data, but this type of communication not suitable when we need update the view only once with single event, also we cannot hold view's reference in ViewModel and call some methods because it will create memory leak.

    I found something similar SingleLiveEvent - but it work only for 1 observer and not for multiple observers.

    --- Update ----

    As @EpicPandaForce said "There is no reason to use LiveData as something that it is not", probably the intent of the question was Communication between view and ViewModel in MVVM with LiveData

    • Bertram Gilfoyle
      Bertram Gilfoyle over 5 years
      Hi, can you clarify the second sentence in your question? What are you trying to achieve actually?
    • Pavel Poley
      Pavel Poley over 5 years
      events like a message, a navigation event or a dialog trigger. similar to EventBus
    • d4vidi
      d4vidi over 5 years
      I've been struggling with the same issue for a while now and could not for the life of me find good information about it. I'm really happy to see others have come across it too. In any case, I might have been able to work out a pretty clean solution. I'll publish it as an answer when ready.
    • Pavel Poley
      Pavel Poley over 5 years
      @d4vidi hope to see solution
    • d4vidi
      d4vidi over 5 years
      @PavelPoley sorry - had to go off grid for a few days. I'll post something asap.
    • Siddhpura Amit
      Siddhpura Amit about 4 years
      @PavelPoley facing same issue, so which one have you decided from below solutions? Have you find any better solution?
    • Michal Vician
      Michal Vician about 3 years
      I successfully use github.com/hadilq/LiveEvent library.
  • Pavel Poley
    Pavel Poley over 5 years
    yes, i thought about giving unique id to every event, nice idea i will check it, thanks.
  • Egor Neliuba
    Egor Neliuba almost 5 years
    Would not work since observers which are in background would not get updated values after coming to foreground.
  • Jason Grife
    Jason Grife over 4 years
    If setValue is never called, then your leaking any observers that are left in unactivedObservers
  • Fantasy_RQG
    Fantasy_RQG over 4 years
    I think WeakReference may solve memory leak problem
  • Jason Grife
    Jason Grife over 4 years
    it may, but why not address the issue with the pattern so you don't have to utilize a WeakReference.
  • emandt
    emandt over 4 years
    Using this library is BEYOND the purpose/scope of main question. The Author asked a solution for LiveData and not a generic workaround/solution to reach his achievement.
  • EpicPandaForce
    EpicPandaForce over 4 years
    LiveData was not designed and is not suitable for this. Everything else is a trick.
  • Nikola Samardzija
    Nikola Samardzija over 4 years
    How is it possible that it is not designed for such a simple use case? For example: ViewModel needs to notify View that error occurred and after that View shows SnackBar. Then start observing again that observer (activity restarted) and you get error message. Do I still need some workarounds or hacks for this use case? Simplest workaround for me would be RxJava.
  • EpicPandaForce
    EpicPandaForce over 4 years
    @Nikola LiveData works like a BehaviorRelay, it will always emit the last emitted value. If you need a PublishRelay, then LiveData will not be a suitable solution. The "official Google sample" uses EventObserver + LiveData<Event<T>> but in reality that's just SingleLiveData with more code, which is already like BehaviorRelay.skip(1) which is just awkward.
  • Phileo99
    Phileo99 almost 3 years
    This problem cannot be solved with LiveData without bringing in some hack or fighting against the framework. The library introduced by @EpicPandaForce is simple, well tested, and most importantly, solved the problem at hand for me.
  • BabyishTank
    BabyishTank almost 3 years
    @EgorNeliuba Would you mind explaining more why this don't work? Isn't preventing the observer getting trigger after coming back to foreground what we are trying to solve?
  • Olsi Saqe
    Olsi Saqe about 2 years
    You are never removing the observers. The removeObserver method is called from supper supplying your private ObserverWrapper type. You need to unwrap that.