Reputation: 42710
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.
MainFragment
is the current active fragment. TrashFragment
is not created yet.MainFragment
added 1 trashed data.MainFragment
with TrashFragment
.TrashFragment
's observer will first receive onChanged
, with 0 trashed dataTrashFragment
's observer will secondly receive onChanged
, with 1 trashed dataWhat 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 code:
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);
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);
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();
}
}
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?
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.
Demo project can be downloaded from https://github.com/yccheok/live-data-problem
Upvotes: 97
Views: 108642
Reputation: 811
I observed most of the time in Fragment, if we are initialing ViewModel globally, which leads to create observer multiple time we call onViewCreated(), So, better do not initialize globally but on onViewCreated() only, so, every time, ViewModel gives you new instance. so, observer also.
Upvotes: 0
Reputation: 450
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.
Upvotes: -1
Reputation: 1577
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.
Upvotes: 9
Reputation: 4140
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
}
);
}
Upvotes: 8
Reputation: 5173
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
Upvotes: 3
Reputation: 545
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
})
Upvotes: 21
Reputation: 20258
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
.
Upvotes: 83
Reputation: 99
I used SingleLiveEvent and works. When fragment/activity is resumed or recreated SingleLiveEvent not throw the event, only when explicitly changes
Upvotes: 8
Reputation: 29867
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.
Upvotes: -3
Reputation: 547
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
Upvotes: 1
Reputation: 2431
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.
Upvotes: 20
Reputation: 2777
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.
Upvotes: 2
Reputation: 134
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);
}
}
});
Upvotes: 6
Reputation: 81549
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.
Upvotes: 11
Reputation: 16238
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.
Upvotes: 45