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

AndroidAndroid Architecture-ComponentsAndroid Livedata

Android Problem Overview


My understanding on LiveData is that, it will trigger observer on the current state change of data, and not a series of history state change of data.

Currently, I have a MainFragment, which perform Room write operation, to change non-trashed data, to trashed data.

I also another TrashFragment, which observes to trashed data.

Consider the following scenario.

  1. There are currently 0 trashed data.
  2. MainFragment is the current active fragment. TrashFragment is not created yet.
  3. MainFragment added 1 trashed data.
  4. Now, there are 1 trashed data
  5. We use navigation drawer, to replace MainFragment with TrashFragment.
  6. TrashFragment's observer will first receive onChanged, with 0 trashed data
  7. Again, TrashFragment's observer will secondly receive onChanged, with 1 trashed data

What is out of my expectation is that, item (6) shouldn't happen. TrashFragment should only receive latest trashed data, which is 1.

Here's my codes


TrashFragment.java

public class TrashFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ...

        noteViewModel.getTrashedNotesLiveData().removeObservers(this);
        noteViewModel.getTrashedNotesLiveData().observe(this, notesObserver);

MainFragment.java

public class MainFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ...

        noteViewModel.getNotesLiveData().removeObservers(this);
        noteViewModel.getNotesLiveData().observe(this, notesObserver);

NoteViewModel .java

public class NoteViewModel extends ViewModel {
    private final LiveData<List<Note>> notesLiveData;
    private final LiveData<List<Note>> trashedNotesLiveData;

    public LiveData<List<Note>> getNotesLiveData() {
        return notesLiveData;
    }

    public LiveData<List<Note>> getTrashedNotesLiveData() {
        return trashedNotesLiveData;
    }

    public NoteViewModel() {
        notesLiveData = NoteplusRoomDatabase.instance().noteDao().getNotes();
        trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
    }
}

Code which deals with Room

public enum NoteRepository {
    INSTANCE;

    public LiveData<List<Note>> getTrashedNotes() {
        NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
        return noteDao.getTrashedNotes();
    }
    
    public LiveData<List<Note>> getNotes() {
        NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
        return noteDao.getNotes();
    }
}
    
@Dao
public abstract class NoteDao {
    @Transaction
    @Query("SELECT * FROM note where trashed = 0")
    public abstract LiveData<List<Note>> getNotes();

    @Transaction
    @Query("SELECT * FROM note where trashed = 1")
    public abstract LiveData<List<Note>> getTrashedNotes();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public abstract long insert(Note note);
}
    
@Database(
        entities = {Note.class},
        version = 1
)
public abstract class NoteplusRoomDatabase extends RoomDatabase {
    private volatile static NoteplusRoomDatabase INSTANCE;

    private static final String NAME = "noteplus";

    public abstract NoteDao noteDao();

    public static NoteplusRoomDatabase instance() {
        if (INSTANCE == null) {
            synchronized (NoteplusRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(
                            NoteplusApplication.instance(),
                            NoteplusRoomDatabase.class,
                            NAME
                    ).build();
                }
            }
        }

        return INSTANCE;
    }
}

Any idea how I can prevent from receiving onChanged twice, for a same data?


Demo

I created a demo project to demonstrate this problem.

As you can see, after I perform write operation (Click on ADD TRASHED NOTE button) in MainFragment, when I switch to TrashFragment, I expect onChanged in TrashFragment will only be called once. However, it is being called twice.

enter image description here

Demo project can be downloaded from https://github.com/yccheok/live-data-problem

Android Solutions


Solution 1 - Android

I have introduced just one change in your code:

noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);

instead of:

noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);

in Fragment's onCreate(Bundle) methods. And now it works seamlessly.

In your version you obtained a reference of NoteViewModel common to both Fragments (from Activity). ViewModel had Observer registered in previous Fragment, I think. Therefore LiveData kept reference to both Observer's (in MainFragment and TrashFragment) and called both values.

So I guess the conclusion might be, that you should obtain ViewModel from ViewModelProviders from:

  • Fragment in Fragment
  • Activity in Activity

Btw.

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

is not necessary in Fragments, however I would advise putting it in onStop.

Solution 2 - Android

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.

Solution 3 - Android

The reason is that in your .observe() method, you passed a fragment as the lifecycle owner. What should have been passed is the viewLifecycleOwner object of the fragment

viewModel.livedata.observe(viewLifecycleOwner, Observer {
        // Do your routine here
    })

Solution 4 - Android

It's not a bug, it's a feature. Read why!

The observers method void onChanged(@Nullable T t) is called twice. That's fine.

The first time it is called upon startup. The second time it is called as soon as Room has loaded the data. Hence, upon the first call the LiveData object is still empty. It is designed this way for good reasons.

Second call

Let's start with the second call, your point 7. The documentation of Room says:

> Room generates all the necessary code to update the LiveData object > when a database is updated. The generated code runs the query > asynchronously on a background thread when needed.

The generated code is an object of the class ComputableLiveData mentioned in other postings. It manages a MutableLiveData object. Upon this LiveData object it calls LiveData::postValue(T value) which then calls LiveData::setValue(T value).

LiveData::setValue(T value) calls LiveData::dispatchingValue(@Nullable ObserverWrapper initiator). This calls LiveData::considerNotify(ObserverWrapper observer) with the observer wrapper as parameter. This finally calls onChanged() upon the observer with the loaded data as parameter.

First call

Now for the first call, your point 6.

You set your observers within the onCreateView() hook method. After this point the lifecycle changes it state twice to come visible, on start and on resume. The internal class LiveData::LifecycleBoundObserver is notified upon such changes of state because it implements the GenericLifecycleObserver interface, which holds one method named void onStateChanged(LifecycleOwner source, Lifecycle.Event event);.

This method calls ObserverWrapper::activeStateChanged(boolean newActive) as LifecycleBoundObserver extends ObserverWrapper. The method activeStateChanged calls dispatchingValue() which in turn calls LiveData::considerNotify(ObserverWrapper observer) with the observer wrapper as parameter. This finally calls onChanged() upon the observer.

All this happens under certain conditions. I admit that I didn't investigated all conditions within the chain of methods. There are two changes of state, but onChanged() is only triggered once, because the conditions check for things like this.

The bottomline here is, that there is a chain of methods, that is triggered upon changes of the lifecycle. This is responsible for the first call.

Bottomline

I think nothing goes wrong with your code. It's just fine, that the observer is called upon creation. So it can fill itself with the initial data of the view model. That's what an observer should do, even if the database part of the view model is still empty upon the first notification.

Usage

The first notification basically tells that the view model is ready for to display, despite it still is not loaded with data from underlying databases. The second notification tells, that this data is ready.

When you think of slow db connections, this is a reasonable approach. You may want to retrieve and display other data from the view model triggered by the notification, that does not come from the database.

Android has a guideline how to deal with slow database loading. They suggest to use placeholders. In this example the gap is that short, that there is no reason to go to such an extend.

Appendix

Both Fragments use there own ComputableLiveData objects, that's why the second object is not preloaded from the first fragment.

Also think of the case of rotation. The data of the view model does not change. It does not trigger a notification. The state changes of the lifecycle alone trigger the notification of the new new view.

Solution 5 - Android

I snatched Vasiliy's fork of your fork of the fork and did some actual debugging to see what happens.

> Might be related to the way ComputableLiveData offloads onActive() computation to Executor.

Close. The way Room's LiveData<List<T>> expose works is that it creates a ComputableLiveData, which keeps track of whether your data set has been invalidated underneath in Room.

trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();

So when the note table is written to, then the InvalidationTracker bound to the LiveData will call invalidate() when a write happens.

  @Override
  public LiveData<List<Note>> getNotes() {
    final String _sql = "SELECT * FROM note where trashed = 0";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return new ComputableLiveData<List<Note>>() {
      private Observer _observer;

      @Override
      protected List<Note> compute() {
        if (_observer == null) {
          _observer = new Observer("note") {
            @Override
            public void onInvalidated(@NonNull Set<String> tables) {
              invalidate();
            }
          };
          __db.getInvalidationTracker().addWeakObserver(_observer);
        }

Now what we need to know is that ComputableLiveData's invalidate() will actually refresh the data set, if the LiveData is active.

// invalidation check always happens on the main thread
@VisibleForTesting
final Runnable mInvalidationRunnable = new Runnable() {
    @MainThread
    @Override
    public void run() {
        boolean isActive = mLiveData.hasActiveObservers();
        if (mInvalid.compareAndSet(false, true)) {
            if (isActive) { // <-- this check here is what's causing you headaches
                mExecutor.execute(mRefreshRunnable);
            }
        }
    }
};

Where liveData.hasActiveObservers() is:

public boolean hasActiveObservers() {
    return mActiveCount > 0;
}

So refreshRunnable actually runs only if there is an active observer (afaik means lifecycle is at least started, and observes the live data).



This means that when you subscribe in TrashFragment, then what happens is that your LiveData is stored in Activity so it is kept alive even when TrashFragment is gone, and retains previous value.

However, when you open TrashFragment, then TrashFragment subscribes, LiveData becomes active, ComputableLiveData checks for invalidation (which is true as it was never re-computed because the live data was not active), computes it asynchronously on background thread, and when it is complete, the value is posted.

So you get two callbacks because:

1.) first "onChanged" call is the previously retained value of the LiveData kept alive in the Activity's ViewModel

2.) second "onChanged" call is the newly evaluated result set from your database, where the computation was triggered by that the live data from Room became active.


So technically this is by design. If you want to ensure you only get the "newest and greatest" value, then you should use a fragment-scoped ViewModel.

You might also want to start observing in onCreateView(), and use viewLifecycle for the lifecycle of your LiveData (this is a new addition so that you don't need to remove observers in onDestroyView().

If it is important that the Fragment sees the latest value even when the Fragment is NOT active and NOT observing it, then as the ViewModel is Activity-scoped, you might want to register an observer in the Activity as well to ensure that there is an active observer on your LiveData.

Solution 6 - Android

My answer is not a solution to this question description but rather to question title. Just title.

If your observer for a LiveData<*> is getting called multiple times then it means you are calling livedata.observe(...) multiple times. This happened to me as I was doing livedata.observe(...) in a method and was calling this method whenever user does some action thus observing liveData again. To solve this I moved livedata.observe(...) to onCreate() lifecycle method.

What was the scenario? The App has a color swatch. When user selects a color I had to make API call to fetch Product Images for that color. So was making API call and was observing livedata in onColorChanged() . When user selects a new color, onColorChanged() would be called again thus observing for livedata changes again.

Edit: The other issue could be passing this instead of viewLifecycleOwner while registering LiveData Observer as pointed out in another answer below. Always use viewLifecycleOwner when observing LiveData in Fragments.

Solution 7 - Android

This is what happens under the hood:

ViewModelProviders.of(getActivity())

As you are using getActivity() this retains your NoteViewModel while the scope of MainActivity is alive so is your trashedNotesLiveData.

When you first open your TrashFragment room queries the db and your trashedNotesLiveData is populated with the trashed value (At the first opening there is only one onChange() call). So this value is cached in trashedNotesLiveData.

Then you come to the main fragment add a few trashed notes and go to the TrashFragment again. This time you are first served with the cached value in trashedNotesLiveData while room makes async query. When query finishes you are brought the latest value. This is why you get two onChange() calls.

So the solution is you need to clean the trashedNotesLiveData before opening TrashFragment. This can either be done in your getTrashedNotesLiveData() method.

public LiveData<List<Note>> getTrashedNotesLiveData() {
    return NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
}

Or you can use something like this SingleLiveEvent

Or you can use a MediatorLiveData which intercepts the Room generated one and returns only distinct values.

final MediatorLiveData<T> distinctLiveData = new MediatorLiveData<>();
    distinctLiveData.addSource(liveData, new Observer<T>() {
        private boolean initialized = false;
        private T lastObject = null;

        @Override
        public void onChanged(@Nullable T t) {
            if (!initialized) {
                initialized = true;
                lastObject = t;
                distinctLiveData.postValue(lastObject);
            } else if (t != null && !t.equals(lastObject)) {
                lastObject = t;
                distinctLiveData.postValue(lastObject);
            }

        }
    });

Solution 8 - Android

Never put an observer inside loops/any place where it gets registered twice. Observers should be put inside onViewCreated / onCreate / any place that gets called only once. OBSERVE ONLY ONCE !

Here is an example of the wrong way :

for(int i=0;i<5;i++){
//THIS IS WRONG, DONT PUT IT INSIDE A LOOP / FUNCTION CALL
    yourviewModel.getYourLiveData().observe(getViewLifecycleOwner(), new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean sBoolean) {
                 //SOME CODE 
            }
 );
}

IT IS WRONG TO PUT IT UNDER SOME FUNCTION THAT GETS CALLED MORE THAN ONCE, like:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
observeMyViewModel();
observeMyViewModel();//THIS IS WRONG, CALLING IT MORE THAN ONCE
}

private void observeMyViewModel(){
  yourviewModel.getYourLiveData().observe(getViewLifecycleOwner(), new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean sBoolean) {
                 //SOME CODE 
            }
 );
}

Solution 9 - Android

I used https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java">SingleLiveEvent</a> and works. When fragment/activity is resumed or recreated SingleLiveEvent not throw the event, only when explicitly changes

Solution 10 - Android

I found out specifically why it's acting the way it is. The observed behavior was onChanged() in the trash fragment is called once the first time you activate the fragment after trashing a note (on fresh app start) and gets called twice when fragment get activated thereafter after a note is trashed.

The double calls happen because:

Call #1: The fragment is transitioning between STOPPED and STARTED in its lifecyle and this causes a notification to be set to the LiveData object (it's a lifecycle observer after all!). The LiveData code calls the the onChanged() handler because it thinks the observer's version of the data needs to be updated (more on this later). Note: the actual update to the data could still be pending at this point causing the onChange() to get called with stale data.

Call #2: Ensues as a result of the query setting the LiveData (normal path). Again the LiveData object thinks the observer's version of the data is stale.

Now why does onChanged() only get called once the very first time the view is activated after app startup? It's because the first time the LiveData version checking code executes as a result of the STOPPED->STARTED transition the live data has never been set to anything and thus LiveData skips informing the observer. Subsequent calls through this code path (see considerNotify() in LiveData.java) execute after the data has been set at least once.

LiveData determines if the observer has stale data by keeping a version number that indicates how many times the data has been set. It also records the version number last sent to the client. When new data is set LiveData can compare these versions to determine if an onChange() call is warranted.

Here's the version #s during the calls to the LiveData version checking code for the 4 calls:

   Ver. Last Seen  Ver. of the     OnChanged()
   by Observer     LiveData        Called?
  --------------   --------------- -----------
1  -1 (never set)  -1 (never set)  N
2  -1              0               Y
3  -1              0               Y
4   0              1               Y

If you're wondering why version last seen by the observer in call 3 is -1 even though onChanged() was called the 2nd time around it's because the observer in calls 1/2 is a different observer than the one in calls 3/4 (the observer is in the fragment which was destroyed when the user went back to the main fragment).

An easy way to avoid confusion regarding the spurious calls that happen as a result of lifecycle transitions is to keep a flag in the fragment intialized to false that indicates if the fragment has been fully resumed. Set that flag to true in the onResume() handler then check to see if that flag is true in your onChanged() handler. That way you can be sure you're responding to events that happened becuase data was truly set.

Solution 11 - Android

If you are looking for a solution to avoid the multiple triggers on popUp the back stack from destination fragment to the original fragment

> My solution is to observe the LiveData at onCreate() of the Fragment lifecycle with lifecycle owner as Activity and remove the observer at > onDestroy() of the Fragment lifecycle

Solution 12 - Android

I'm not sure if this issue is still active.

But the main perpetrator was a bug inside the fragment Lifecycle owner for fragments which was not cleared when the view was destroyed.

Previously you would have to implement your own lyfecycle owner that would move the state to destroyed when onDestroyView would be called.

This should no longer be the case if you target and compile with at least API 28

Solution 13 - Android

Here's how to fix this in kotlin:

In room DAO, use Flow<List<T>> instead of LiveData<List<T>>.

So, in the OP's example we can use:

@Query("SELECT * FROM note where trashed = 1")
fun getTrashedNotes(): Flow<List<Note>>

instead of

@Query("SELECT * FROM note where trashed = 1")
fun getTrashedNotes(): LiveData<List<Note>>

Then in viewModel, we can use val list = dao.getTrashedNotes().asLiveData().

So OP's viewModel will be:

val trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes().asLiveData()

And rest of the flows after viewModel remains the same.

Reason why this works:

Flow, unlike liveData, is not lifecycle aware. So, even if the fragment is not created, flow's value will be up-to date.

Solution 14 - Android

The solution I had was simply to start observing data when I need it and remove the observer as soon as it has retrieved the data. You won't get double triggering this way.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionCheok Yan ChengView Question on Stackoverflow
Solution 1 - AndroidR. ZagórskiView Answer on Stackoverflow
Solution 2 - AndroidVasiliyView Answer on Stackoverflow
Solution 3 - AndroidJoMView Answer on Stackoverflow
Solution 4 - AndroidBlcknxView Answer on Stackoverflow
Solution 5 - AndroidEpicPandaForceView Answer on Stackoverflow
Solution 6 - AndroidRamakrishna JoshiView Answer on Stackoverflow
Solution 7 - AndroidkatharmoiView Answer on Stackoverflow
Solution 8 - AndroidReejesh PKView Answer on Stackoverflow
Solution 9 - AndroidOrnolis Vázquez ThompsonView Answer on Stackoverflow
Solution 10 - AndroidJJFView Answer on Stackoverflow
Solution 11 - AndroidMuhamed Riyas MView Answer on Stackoverflow
Solution 12 - AndroidmvbrenesView Answer on Stackoverflow
Solution 13 - AndroidJazib KhanView Answer on Stackoverflow
Solution 14 - AndroidJohannView Answer on Stackoverflow