Search code examples
androidrealmandroid-architecture-componentsandroid-livedata

Observer for Android LiveData not called (but it is with observeForever)


I have a Fragment on a ViewPager which shows a list of items. Clicking one, opens a new details-Activity where you can manipulate this item (e.g. change the name). When you return to the list, I want the list / item to be reflect the updates.

I'm using a combination of Realm and LiveData (RealmChangeListener is added and updates the value of the actual LiveData) which works fine.

Plus, I'm using the "reobserve" approach (#3) from https://medium.com/@BladeCoder/architecture-components-pitfalls-part-1-9300dd969808 which works fine when the fragments of the ViewPager are re-added.


When altering the list-items on the list-fragment itself, the observer is called and everything is fine. But when changes are made from a different activity (details) the LiveData-observer is not called.

However, when using observeForever() instead of observe(LifecycleOwner) the observer is called even when changes are made from the other activity (which is what I want).


The fragment is only stopped (and resumed when you come back from the details to the list). It is not detached, not destroyed, its views still exist etc. so the observeForever-observer can update the UI just fine (even when not visible).

But I'd like to use the "add and forget"-approach of the normal observe() where I don't need to remove the observer.


Does anyone has an idea why the observer is not called? As mentioned the fragment is only stopped and the views do still exist (and it is called when the changes happen from the same activity but not in a different one).

Because of observeForever() working, I can only think of a buggy lifecycle where the observer is marked as "stopped" but not as "resumed" when coming back?


edit 1

As asked, here's the code to get the RealmResult as a LiveData (from a blog post at realm):

Realm.getDefaultInstance()
  .where<Model>()
  .findAllAsync()
  .asLiveData()

fun <T : RealmModel> RealmResults<T>.asLiveData() = RealmResultsRealmData(this)

class RealmResultsRealmData<T : RealmModel>(private val results: RealmResults<T>) : LiveData<RealmResults<T>>() {

    private val listener = RealmChangeListener<RealmResults<T>> { realmResults ->
        value = if (realmResults.isValid) realmResults else null
    }

    // region LiveData

    override fun onActive() {
        results.addChangeListener(listener)
    }

    override fun onInactive() {
        results.removeChangeListener(listener)
    }

    // endregion
}

This is working just fine. My ViewModel calls realm.close() in onCleared() so this should be fine as well.

As mentioned, I don't think the problem is at the realm-side because it works when using observeForever() but not when using the normal observe() on the LiveData. Realm just fills the LiveData and this is verified working (using observeForever()).

My best guess ist still the lifecycle of the observer when using observe(). But I don't get why it's not called when the fragment gets "resumed". The docs say "observers also receive an update when they change from an inactive to an active state".

It is not defined whats an "active" state is but I guess it's at least "started" and my fragment gets to this state when I come back from the details-activity. So the observer should be called with the latest value of the LiveData? In contrast, when using observeForever() the observer is called right away while I'm still at the details-activity (because the fragment is only stopped but still active?!).


Solution

  • It seems that addChangeListener() does not call the listener with the current value.

    The LiveRealmResults-class from the realm-examples does this manually in the constructor (when the result is already loaded, so only for synchronous queries).

    if (results.isLoaded) {
        // we should not notify observers when results aren't ready yet (async query).
        // however, synchronous query should be set explicitly.
        value = results
    }
    

    For async-ones, the listener will only be called when the query returns (so this works most of the times because the LiveData is already active (onActive() has been called from the system and the listener has been added).


    In my case (where the model is changed while the LiveData is inactive), the listener is not called when it gets active again (as said, addChangeListener() does not call the listener with the current value).

    So my UI (observing the LiveData) is not notified and still shows the old data.


    Workaround

    I workaround this, by adding the same lines from the example-constructor to onActive() (copied from the example) as well. This way, the listener is called with the current value when LiveData gets active (but not on the initial call when the result is not loaded - so no double calls).

    override fun onActive() {
        super.onActive()
    
        if (results.isValid) { // invalidated results can no longer be observed.
            results.addChangeListener(listener)
    
            if (results.isLoaded) {
                value = results
            }
        }
    }