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.
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.
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:
- Ensure that REPRODUCE_BUG is set to true in MainFragment
- Install the app
- Click on "add trashed note" button
- Switch to TrashFragment
- Note that there was just one notification form LiveData with correct value
- Switch to MainFragment
- Click on "add trashed note" button
- Switch to TrashFragment
- 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.
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.
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.
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.
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.
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.
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.
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
})
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.
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
.