Room : LiveData from Dao will trigger Observer.onChanged on every Update, even if the LiveData value has no change
Asked Answered
H

6

34

I found that the LiveData returned by Dao will call its observer whenever the row is updated in DB, even if the LiveData value is obviously not changed.

Consider a situation like the following example :

Example entity

@Entity
public class User {
    public long id;
    public String name;
    // example for other variables
    public Date lastActiveDateTime;
}

Example Dao

@Dao
public interface UserDao {
    // I am only interested in the user name
    @Query("SELECT name From User")
    LiveData<List<String>> getAllNamesOfUser();

    @Update(onConflict = OnConflictStrategy.REPLACE)
    void updateUser(User user);
}

Somewhere in background thread

UserDao userDao = //.... getting the dao
User user = // obtain from dao....
user.lastActiveDateTime = new Date(); // no change to user.name
userDao.updateUser(user);

Somewhere in UI

// omitted ViewModel for simplicity
userDao.getAllNamesOfUser().observe(this, new Observer<List<String>> {
    @Override
    public void onChanged(@Nullable List<String> userNames) {
        // this will be called whenever the background thread called updateUser. 
        // If user.name is not changed, it will be called with userNames 
        // with the same value again and again when lastActiveDateTime changed.
    }
});

In this example, the ui is only interested to user name so the query for LiveData only includes the name field. However the observer.onChanged will still be called on Dao Update even only other fields are updated. (In fact, if I do not make any change to User entity and call UserDao.updateUser, the observer.onChanged will still be called)

Is this the designed behaviour of Dao LiveData in Room? Is there any chance I can work around this, so that the observer will only be called when the selected field is updated?


Edit : I changed to use the following query to update the lastActiveDateTime value as KuLdip PaTel in comment suggest. The observer of LiveData of user name is still called.

@Query("UPDATE User set lastActiveDateTime = :lastActiveDateTime where id = :id")
void updateLastActiveDateTime(Date lastActiveDateTime, int id);
Hydrostat answered 10/11, 2017 at 4:24 Comment(3)
look your @Dao interface for update user, you can replace every time user you not Actually update date field, you add all field. please change in update Query that may help you.Dirk
@KuLdipPaTel Please see the edit. I used Query to update the field instead and still got the same result.Hydrostat
@Hydrostat had you solved your problem? I have the same, but i couldn't solve. May you add solution in your question, pleaseKabyle
D
9

This situation is known as false positive notification of observer. Please check point number 7 mentioned in the link to avoid such issue.

Below example is written in kotlin but you can use its java version to get it work.

fun <T> LiveData<T>.getDistinct(): LiveData<T> {
    val distinctLiveData = MediatorLiveData<T>()
    distinctLiveData.addSource(this, object : Observer<T> {
        private var initialized = false
        private var lastObj: T? = null
        override fun onChanged(obj: T?) {
            if (!initialized) {
                initialized = true
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            } else if ((obj == null && lastObj != null) 
                       || obj != lastObj) {
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            }
        }
    })
    return distinctLiveData
}
Diarmit answered 17/11, 2017 at 12:9 Comment(3)
While this is a workaround for what I'd consider to be a bug in Room, it's incredibly inefficient to return every value from the table and filter the results in the app. You're effectively keeping the entire table in memory. What's the point of even using the database at this point?Bizerte
@Pinakin please, may you write code on Java too? Because from your link i couldn't set solution.Kabyle
@Kabyle , Y would you require that to be in java , when you can have kotlin and java in same project. Well you can use distinct keyword of java to implement the same in java. See below link for example. geeksforgeeks.org/stream-distinct-javaDiarmit
B
19

There is simple solution in Transformations method distinctUntilChanged.expose new data only if data was changed.

In this case we get data only when it changes in source:

LiveData<YourType> getData(){
    return Transformations.distinctUntilChanged(LiveData<YourType> source));
}

But for Event cases is better to use this: https://mcmap.net/q/451988/-livedata-prevent-receive-the-last-value-when-start-observing

Bulley answered 18/3, 2019 at 15:30 Comment(1)
i am trying this, it says cannot be resolved even after adding the package..Curtate
D
9

This situation is known as false positive notification of observer. Please check point number 7 mentioned in the link to avoid such issue.

Below example is written in kotlin but you can use its java version to get it work.

fun <T> LiveData<T>.getDistinct(): LiveData<T> {
    val distinctLiveData = MediatorLiveData<T>()
    distinctLiveData.addSource(this, object : Observer<T> {
        private var initialized = false
        private var lastObj: T? = null
        override fun onChanged(obj: T?) {
            if (!initialized) {
                initialized = true
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            } else if ((obj == null && lastObj != null) 
                       || obj != lastObj) {
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            }
        }
    })
    return distinctLiveData
}
Diarmit answered 17/11, 2017 at 12:9 Comment(3)
While this is a workaround for what I'd consider to be a bug in Room, it's incredibly inefficient to return every value from the table and filter the results in the app. You're effectively keeping the entire table in memory. What's the point of even using the database at this point?Bizerte
@Pinakin please, may you write code on Java too? Because from your link i couldn't set solution.Kabyle
@Kabyle , Y would you require that to be in java , when you can have kotlin and java in same project. Well you can use distinct keyword of java to implement the same in java. See below link for example. geeksforgeeks.org/stream-distinct-javaDiarmit
K
3

I stuck with the same problem.

What i did wrong:

1) creating anonimous object:

private LiveData<List<WordsTableEntity>> listLiveData;
// listLiveData = ... //init our LiveData...
listLiveData.observe(this, new Observer<List<WordsTableEntity>>() {
        @Override
        public void onChanged(@Nullable List<WordsTableEntity> wordsTableEntities) {

        }
    });

In my case, I called the method several times in which this line was located.

From the docs i supposed, that new Observers take data from LiveData. Because of that, author could receive few onChanged methods from few new anonimous Observers, if he set observe userDao.getAllNamesOfUser().observe(this, new Observer that way.

Its will be better to create named Observer object before LiveData.observe(... and once

@Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        observer = new Observer<List<WordsTableEntity>>() {
            @Override
            public void onChanged(@Nullable List<WordsTableEntity> wordsTableEntities) {
                adapter.setWordsTableEntities(wordsTableEntities);
                progressBar.setVisibility(View.GONE);
            }
        };
    }

and then set it LiveData.observe(observer and we receive data from LieData first time and then, when data will be changed.

2) Observing one Observe object multiple times

public void callMethodMultipleTimes(String searchText) {
            listLiveData = App.getRepositoryRoomDB().searchDataExceptChapter(searchText);
            listLiveData.observe(this, observer);
    }

I calling this method multiple times and debug showed me, that i was adding my observer as many times, as i called callMethodMultipleTimes();

Our listLiveData is a global variable and it lives. It changes the object reference here

listLiveData = App.getRepositoryRoomDB().searchDataExceptChapter(searchText);

, but the old object in memory is not immediately deleted

This will be fixed, if we call listLiveData.removeObserver(observer); before

listLiveData = App.getRepositoryRoomDB().searchDataExceptChapter(searchText);

And returning to 1) - we can not call listLiveData.removeObserver(our anonimous Observer); because we do not have an anonymous object reference.

So, in the result we can do so:

private Observer observer;
private LiveData<List<WordsTableEntity>> listLiveData;
@Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        observer = new Observer<List<WordsTableEntity>>() {
            @Override
            public void onChanged(@Nullable List<WordsTableEntity> wordsTableEntities) {
                adapter.setWordsTableEntities(wordsTableEntities);
                progressBar.setVisibility(View.GONE);
            }
        };
    }

public void searchText(String searchText) {
            if (listLiveData != null){
                listLiveData.removeObservers(this);
            }
            listLiveData = App.getRepositoryRoomDB().searchDataExceptChapter(searchText);
            listLiveData.observe(this, observer);
    }

I didn't use distinct functions. In my case it works without distinct.

I hope my case will help someone.

P.S. Version of libraries

    // Room components
    implementation "android.arch.persistence.room:runtime:1.1.1"
    annotationProcessor "android.arch.persistence.room:compiler:1.1.1"
    androidTestImplementation "android.arch.persistence.room:testing:1.1.1"

    // Lifecycle components
    implementation "android.arch.lifecycle:extensions:1.1.1"
    annotationProcessor "android.arch.lifecycle:compiler:1.1.1"
Kabyle answered 10/1, 2019 at 1:12 Comment(0)
T
2

Currently there is no way to stop triggering Observer.onChanged which is why I think the LiveData will be useless for most of the queries that are using some joins. Like @Pinakin mentioned there is a MediatorLiveData but this is just a filter and the data still gets loaded on every change. Imagine having 3 left joins in 1 query where you only need a field or two from those joins. In case you implement PagedList every time any record from those 4 tables (main + 3 joined tables) gets updated, the query will be called again. This is OK for some some tables with small amount of data, but correct me if I wrong this would be bad in case of bigger tables. It would be best if we would have some way of setting the query to be refreshed only if the main table is updated or ideally to have a way to refresh only if fields from that query are updated in the database.

Transfiguration answered 28/2, 2019 at 7:20 Comment(1)
M
0

Avoid false positive notifications for observable queries
Let’s say that you want to get a user based on the user id in an observable query:

@Query(“SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): LiveData<User>

You’ll get a new emission of the User object whenever that user updates. But you will also get the same object when other changes (deletes, updates or inserts) occur on the Users table that has nothing to do with the User you’re interested in, resulting in false-positive notifications. Even more, if your query involves multiple tables, you’ll get a new emission whenever something changed in any of them.

If your query returns a LiveData, you can use a MediatorLiveData that only allows distinct object emissions from a source.

fun <T> LiveData<T>.getDistinct(): LiveData<T> {
    val distinctLiveData = MediatorLiveData<T>()
    distinctLiveData.addSource(this, object : Observer<T> {
        private var initialized = false
        private var lastObj: T? = null
        override fun onChanged(obj: T?) {
            if (!initialized) {
                initialized = true
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            } else if ((obj == null && lastObj != null) 
                       || obj != lastObj) {
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            }
        }
    })
    return distinctLiveData
}

In your DAOs, make method that returns the distinct LiveData public and the method that queries the database protected.

@Dao
 abstract class UserDao : BaseDao<User>() {
   @Query(“SELECT * FROM Users WHERE userid = :id”)
   protected abstract fun getUserById(id: String): LiveData<User>
   fun getDistinctUserById(id: String): 
         LiveData<User> = getUserById(id).getDistinct()
}

See more of the code here and also in Java.

Massive answered 30/11, 2019 at 19:29 Comment(0)
C
0

In Kotlin use extension function: fun <X> LiveData<X>.distinctUntilChanged(): LiveData<X> from androidx.lifecycle.Transformations.kt.

Example:

var myLiveData: LiveData<Boolean> = myMutableLivedata.distinctUntilChanged()
Cyclopropane answered 16/5 at 15:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.