Realm-java: RealmChangeListener on empty RealmResults never fires

Created on 6 Jun 2017  Â·  25Comments  Â·  Source: realm/realm-java

Goal

Receive change updates on a RealmQuery

Expected Results

When the RealmQuery changes, the RealmChangeListener is fired

Actual Results

The RealmChangeListener is never fired

Steps & Code to Reproduce

I create a query (realm.where(Test.class).findAll()), and add a RealmChangeListener to the returned RealmResults (I maintain a strong reference to the RealmResults).

If the query was empty when I made it, the RealmChangeListener never fires, no matter how many times I insert an object that matches the query after that. If there is an object when the query is made, the the RealmChangeListener does fire, even if all the objects are removed and then another one is inserted.

Version of Realm and tooling

Realm version(s): 3.3.1

Realm sync feature enabled: no

Android Studio version: 3.0 Canary 3

Which Android version and device: Pixel 7.1.2

Reproduction-Required T-Help

All 25 comments

My temporary fix is right before I make the query, I insert a dummy object. Obviously not a great solution...

Hello!
I made a test below but it could not reproduce your issue:

    @Test
    @RunTestInLooperThread
    public void addChangeListener_initWithEmptyResults() {
        Realm realm = looperThread.getRealm();
        RealmResults<AllTypes> results = realm.where(AllTypes.class).findAll();
        assertTrue(results.isEmpty());
        results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
            @Override
            public void onChange(RealmResults<AllTypes> allTypes) {
                assertEquals(1, allTypes.size());
                looperThread.testComplete();
            }
        });
        realm.executeTransactionAsync(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                realm.createObject(AllTypes.class);
            }
        });
    }

Is it possible for you to make a simple project to show the issue?

@beeender I haven't been able to repro it in another project, but I've tracked it down to what I think the issue is (although I don't know why it would cause an issue).

Here's my sequence of events (I'm using rx 2.0):

  1. Flowable.defer, subscribing on a HandlerThread
  2. Realm.where(Test.class).findAll() on the same thread
  3. Pass that RealmResults to a FlowableOnSubscriber
  4. In FlowableOnSubscriber.subscribe I emit the RealmResults and then add a RealmChangeListener to it, which emits the RealmResults when it fires
  5. I filter the Flowable on RealmResults.isLoaded
  6. I map emissions to RealmResults.createSnapshot
  7. I filter by OrderedRealmCollectionSnapshot.size != 0
  8. I take the last OrderedRealmCollectionSnapshot that was emitted in a 30 second window
  9. Copy the snapshot to an unmanaged list and do something with it
  10. Call OrderedRealmCollectionSnapshot.deleteAllFromRealm in a transaction

If I follow these steps, then the RealmChangeListener never fires. If I follow steps 1-6 and don't do step 7 it does fire ¯_(ツ)_/¯

UPDATE: Some things that I forgot to mention are that the issue occurs only if the RealmResults from step 3 is empty initially, and that after step 7, a Test object is inserted, but the RealmChangeListener still doesn't fire after that.

I filter by OrderedRealmCollectionSnapshot.size != 0

But why? The snapshot is never updated, so of course you'll never receive an emission...

@Zhuinden just to clarify, I'm calling filter on a Flowable<OrderedRealmCollectionSnapshot>, which will only emit if the predicate is true (in this case if OrderedRealmCollectionSnapshot.size > 0).

The upstream should still emit, because it is tied to the RealmChangeListener.

I did forget to mention a few things in my post above; I will update it now.

I still don't understand why createSnapshot() is involved. You might want to include more code.

FWIW the issue still occurs if I keep it as a RealmResults.

I added some steps above to delineate more of what I'm doing.

I use createSnapshot because I want a stable results set.

The objects will still become invalidated inside it if they're deleted, ya know.

Deletion is fine. I just want to make sure other objects aren't added.

I changed my process to copy from the RealmResults, and do an in query later to delete them. Less performant, but it works.

I'll have to revisit later, but I believe that the filter on non empty (this time on the unmanaged list) still caused the issue where the RealmChangeListener wouldn't fire. At this point not sure if it's a realm issue, an rx issue, or both. I'll update when I make a sample project.

in step 4, do you keep a strong reference to the listener added to RealmResults?

Sort of; it's captured in a lambda.

@eygraber are you able to reproduce it with a sample project? thanks!

@beeender I haven't been able to set aside time for it yet.

@eygraber Any progress?

@kneth sorry I haven't got a chance to try. Crazy sprints at work.

@eygraber Any news to share?

I still think it's caused by the snapshot() calls.

Hey @eygraber,
Haven't heard from you in a while. I'm going to close this issue. By all means, if this problem recurs or anything else comes up, feel free to open a new one.
Cheers,
-blake

Hello people. I have the same issue.

No, you probably have a different but similar problem.

This is part of my code.

This is presenter

class FavoritePresenter(private val view: FavoriteContract.FavoriteView,
private val baseContext: Context?) :
FavoriteContract.FavoritePresenter,
RealmChangeListener> {

private val realm = Realm.getDefaultInstance()

init {
    view.setPresenter(this)
}

override fun start() {
    realm.where(ClippingModel::class.java).findAll().addChangeListener(this)
}

override fun onChange(t: RealmResults<ClippingModel>) {
    if (!realm.isInTransaction) {
        realm.beginTransaction()
        val clippings = realm.copyFromRealm(t)
        realm.commitTransaction()
        view.onShowClippingCollection(clippings)
    }
}

}

This is fragment

class FavoriteFragment : BaseFragment(), FavoriteContract.FavoriteView {

private lateinit var presenter: FavoriteContract.FavoritePresenter
private var clippingAdapter: ClippingRVAdapter? = null

companion object {
    fun newInstance(): FavoriteFragment = FavoriteFragment()
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    FavoritePresenter(this, context)
    return inflater.inflate(R.layout.fragment_favorite, container, false)
}

override fun onStart() {
    super.onStart()
    presenter.start()
}

}

_I am never getting callback in onChange() if RealmResults is empty._

See https://realm.io/docs/java/latest/api/io/realm/RealmResults.html

Adds a change listener to this RealmResults.
Registering a change listener will not prevent the underlying RealmResults from being garbage collected. If the RealmResults is garbage collected, the change listener will stop being triggered. To avoid this, keep a strong reference for as long as appropriate e.g. in a class variable.

 public class MyActivity extends Activity {

     private RealmResults<Person> results; // Strong reference to keep listeners alive

     @Override
     protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       results = realm.where(Person.class).findAllAsync();
       results.addChangeListener(new RealmChangeListener<RealmResults<Person>>() {
           @Override
           public void onChange(RealmResults<Person> persons) {
               // React to change
           }
       });
     }
 }

On top of that, you're calling copyFromRealm instead of, like, copyToRealmOrUpdate or insertOrUpdate or anything that actually writes into the db. Also, running synchronous transaction inside a change listener?

Are you trying to get an infinite loop? 😕

What are you doing???

Ok, it works perfectly. Thanks for help @Zhuinden

I still don't know why you have a transaction in your change listener though, you don't need a transaction to read things

Yep, i forgot to refactor it ))

Was this page helpful?
0 / 5 - 0 ratings