Skip to content

Commit

Permalink
Implement fine grained notification API
Browse files Browse the repository at this point in the history
- Add RealmObservable and RealmCollectionObservable interfaces.
- Enable detailed change information for RealmResults through
  OrderedCollectionChange interface.
- Fix a bug in the ObserverPairList which could cause the removed
  listener gets called if it was removed during previous listener
  iteration.

Fix #989
  • Loading branch information
beeender committed Feb 20, 2017
1 parent c85964f commit 5d9401a
Show file tree
Hide file tree
Showing 21 changed files with 979 additions and 87 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
### Breaking changes

* `RealmResults.distinct()` returns a new `RealmResults` object instead of filtering on the original object (#2947).
* `RealmResults` is auto-updated continuously. Any transaction on the current thread which may have an impact on the order or elements of the `RealmResults` will change the `RealmResults` immediately instead of change it in the next event loop. The standard `RealmResults.iterator()` will continue to work as normal, which means that you can still delete or modify elements without impacting the iterator. The same is not true for simple for-loops. In some cases a simple for-loop will not work (https://realm.io/docs/java/2.3.1/api/io/realm/OrderedRealmCollection.html#loops), and you must use the new createSnapshot() method.
* `RealmResults` is auto-updated continuously. Any transaction on the current thread which may have an impact on the order or elements of the `RealmResults` will change the `RealmResults` immediately instead of change it in the next event loop. The standard `RealmResults.iterator()` will continue to work as normal, which means that you can still delete or modify elements without impacting the iterator. The same is not true for simple for-loops. In some cases a simple for-loop will not work (https://realm.io/docs/java/latest/api/io/realm/OrderedRealmCollection.html#loops), and you must use the new createSnapshot() method.

### Enhancements

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
/*
* Copyright 2017 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.realm;

import android.support.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.CountDownLatch;

import io.realm.entities.AllTypes;
import io.realm.rule.RunInLooperThread;
import io.realm.rule.RunTestInLooperThread;
import io.realm.rule.TestRealmConfigurationFactory;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertArrayEquals;

// Tests for the ordered collection fine grained notifications.
// This should be expanded to test the notifications for RealmList as well in the future.
@RunWith(AndroidJUnit4.class)
public class OrderedCollectionChangeSetTests {

@Rule
public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory();
@Rule
public final RunInLooperThread looperThread = new RunInLooperThread();

@Before
public void setUp() {
}

@After
public void tearDown() {
}

private void populateData(Realm realm, int testSize) {
realm.beginTransaction();
for (int i = 0; i < testSize; i++) {
realm.createObject(AllTypes.class).setColumnLong(i);
}
realm.commitTransaction();
}

// The args should be [startIndex1, length1, startIndex2, length2, ...]
private void checkRanges(OrderedCollectionChangeSet.Range[] ranges, long... indexAndLen) {
if ((indexAndLen.length % 2 != 0)) {
fail("The 'indexAndLen' array length is not an even number.");
}
if (ranges.length != indexAndLen.length / 2) {
fail("The lengths of 'ranges' and 'indexAndLen' don't match.");
}
for (int i = 0; i < ranges.length; i++) {
OrderedCollectionChangeSet.Range range = ranges[i];
long startIndex = indexAndLen[i * 2];
long length = indexAndLen[i * 2 + 1];
if (range.startIndex != startIndex || range.length != length) {
fail("Range at index " + i + " doesn't match start index " + startIndex + " length " + length + ".");
}
}
}

// Deletes AllTypes objects which's columnLong is in the indices array.
private void deleteObjects(Realm realm, long... indices) {
for (long index : indices) {
realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, index).findFirst().deleteFromRealm();
}
}

// Creates AllTypes objects with columnLong set to the value elements in indices array.
private void createObjects(Realm realm, long... indices) {
for (long index : indices) {
realm.createObject(AllTypes.class).setColumnLong(index);
}
}

// Modifies AllTypes objects which's columnLong is in the indices array.
private void modifyObjects(Realm realm, long... indices) {
for (long index : indices) {
AllTypes obj = realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, index).findFirst();
assertNotNull(obj);
obj.setColumnString("modified");
}
}

@Test
@RunTestInLooperThread
public void deletion() {
Realm realm = looperThread.realm;
populateData(realm, 10);
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG);
results.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> collection, OrderedCollectionChangeSet changes) {
checkRanges(changes.getDeletionRanges(),
0, 1,
2, 3,
8, 2);
assertArrayEquals(changes.getDeletions(), new long[]{0, 2, 3, 4, 8, 9});
assertEquals(0, changes.getChangeRanges().length);
assertEquals(0, changes.getInsertionRanges().length);
assertEquals(0, changes.getChanges().length);
assertEquals(0, changes.getInsertions().length);
looperThread.testComplete();
}
});

realm.beginTransaction();
deleteObjects(realm,
0,
2, 3, 4,
8, 9);
realm.commitTransaction();
}

@Test
@RunTestInLooperThread
public void insertion() {
Realm realm = looperThread.realm;
realm.beginTransaction();
createObjects(realm, 0, 2, 5, 6, 7, 9);
realm.commitTransaction();
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG);
results.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> collection, OrderedCollectionChangeSet changes) {
checkRanges(changes.getInsertionRanges(),
1, 1,
3, 2,
8, 1);
assertArrayEquals(changes.getInsertions(), new long[]{1, 3, 4, 8});
assertEquals(0, changes.getChangeRanges().length);
assertEquals(0, changes.getDeletionRanges().length);
assertEquals(0, changes.getChanges().length);
assertEquals(0, changes.getDeletions().length);
looperThread.testComplete();
}
});

realm.beginTransaction();
createObjects(realm,
1,
3, 4,
8);
realm.commitTransaction();
}

@Test
@RunTestInLooperThread
public void changes() {
Realm realm = looperThread.realm;
populateData(realm, 10);
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG);
results.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> collection, OrderedCollectionChangeSet changes) {
checkRanges(changes.getChangeRanges(),
0, 1,
2, 3,
8, 2);
assertArrayEquals(changes.getChanges(), new long[]{0, 2, 3, 4, 8, 9});
assertEquals(0, changes.getInsertionRanges().length);
assertEquals(0, changes.getDeletionRanges().length);
assertEquals(0, changes.getInsertions().length);
assertEquals(0, changes.getDeletions().length);
looperThread.testComplete();
}
});

realm.beginTransaction();
modifyObjects(realm,
0,
2, 3, 4,
8, 9);
realm.commitTransaction();
}

@Test
@RunTestInLooperThread
public void moves() {
Realm realm = looperThread.realm;
populateData(realm, 10);
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG);
results.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> collection, OrderedCollectionChangeSet changes) {
checkRanges(changes.getDeletionRanges(),
0, 1,
9, 1);
assertArrayEquals(changes.getDeletions(), new long[]{0, 9});
checkRanges(changes.getInsertionRanges(),
0, 1,
9, 1);
assertArrayEquals(changes.getInsertions(), new long[]{0, 9});
assertEquals(0, changes.getChangeRanges().length);
assertEquals(0, changes.getChanges().length);
looperThread.testComplete();
}
});
realm.beginTransaction();
realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, 0).findFirst().setColumnLong(10);
realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, 9).findFirst().setColumnLong(0);
realm.commitTransaction();
}

@Test
@RunTestInLooperThread
public void mixed_changes() {
Realm realm = looperThread.realm;
populateData(realm, 10);
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG);
results.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> collection, OrderedCollectionChangeSet changes) {
checkRanges(changes.getDeletionRanges(),
0, 2,
5, 1);
assertArrayEquals(changes.getDeletions(), new long[]{0, 1, 5});

checkRanges(changes.getInsertionRanges(),
0, 2,
9, 2);
assertArrayEquals(changes.getInsertions(), new long[]{0, 1, 9, 10});

checkRanges(changes.getChangeRanges(),
3, 2,
8, 1);
assertArrayEquals(changes.getChanges(), new long[]{3, 4, 8});

looperThread.testComplete();
}
});

realm.beginTransaction();
createObjects(realm, 11, 12, -1, -2);
deleteObjects(realm, 0, 1, 5);
modifyObjects(realm, 12, 3, 4, 9);
realm.commitTransaction();
// After transaction, '*' means the object has been modified. 12 has been modified as well, but it is created
// and modified in the same transaction, should not be counted in the changes range.
// [-1, -2, 2, *3, *4, 6, 7, 8, *9, 11, 12]
}

// The change set should empty when the async query returns at the first time.
@Test
@RunTestInLooperThread
public void emptyChangeSet_findAllAsync(){
Realm realm = looperThread.realm;
populateData(realm, 10);
final RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllSortedAsync(AllTypes.FIELD_LONG);
results.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> collection, OrderedCollectionChangeSet changes) {
assertSame(collection, results);
assertEquals(9, collection.size());
assertNull(changes);
looperThread.testComplete();
}
});

final CountDownLatch bgDeletionLatch = new CountDownLatch(1);
new Thread(new Runnable() {
@Override
public void run() {
Realm realm = Realm.getInstance(looperThread.realmConfiguration) ;
realm.beginTransaction();
realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, 0).findFirst().deleteFromRealm();
realm.commitTransaction();
realm.close();
bgDeletionLatch.countDown();
}
}).start();
TestHelper.awaitOrFail(bgDeletionLatch);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ public void run() {
@UiThreadTest
public void addChangeListener_null() {
try {
collection.addChangeListener(null);
collection.addChangeListener((RealmChangeListener)null);
fail();
} catch (IllegalArgumentException ignored) {
}
Expand Down Expand Up @@ -1024,7 +1024,7 @@ public void run() {
@UiThreadTest
public void removeChangeListener_null() {
try {
collection.removeChangeListener(null);
collection.removeChangeListener((RealmChangeListener)null);
fail();
} catch (IllegalArgumentException ignored) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ public void onChange(Collection collection1) {
}

private static class TestIterator extends Collection.Iterator<Integer> {
public TestIterator(Collection collection) {
TestIterator(Collection collection) {
super(collection);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,15 @@ public void remove() {

// Create a new Integer 1 to see if the equality is checked by the same object.
//noinspection UnnecessaryBoxing
pair = new TestObserverPair(new Integer(1), testListener);
observerPairs.remove(pair);
observerPairs.remove(new Integer(1), testListener);
assertEquals(1, observerPairs.size());

// Different listener
pair = new TestObserverPair(ONE, new TestListener());
observerPairs.remove(pair);
observerPairs.remove(ONE, new TestListener());
assertEquals(1, observerPairs.size());

// Should remove now
pair = new TestObserverPair(ONE, testListener);
observerPairs.remove(pair);
observerPairs.remove(ONE, testListener);
assertEquals(0, observerPairs.size());
}

Expand Down Expand Up @@ -240,7 +237,8 @@ public void onCalled(TestObserverPair pair, Object observer) {
public void foreach_canRemove() {
final AtomicInteger count = new AtomicInteger(0);
final TestObserverPair pair1 = new TestObserverPair(ONE, new TestListener());
final TestObserverPair pair2 = new TestObserverPair(TWO, new TestListener());
final TestListener listener2 = new TestListener();
final TestObserverPair pair2 = new TestObserverPair(TWO, listener2);
final TestObserverPair pair3 = new TestObserverPair(THREE, new TestListener());
observerPairs.add(pair1);
observerPairs.add(pair2);
Expand All @@ -250,7 +248,7 @@ public void foreach_canRemove() {
@Override
public void onCalled(TestObserverPair pair, Object observer) {
assertFalse(((Integer) observer) == 2);
observerPairs.remove(pair2);
observerPairs.remove(TWO, listener2);
count.getAndIncrement();
}
});
Expand Down
2 changes: 1 addition & 1 deletion realm/realm-library/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ set(classes_LIST
io.realm.internal.TableQuery io.realm.internal.SharedRealm io.realm.internal.TestUtil
io.realm.log.LogLevel io.realm.log.RealmLog io.realm.Property io.realm.RealmSchema
io.realm.RealmObjectSchema io.realm.internal.Collection
io.realm.internal.NativeObjectReference
io.realm.internal.NativeObjectReference io.realm.internal.CollectionChangeSet
)
# /./ is the workaround for the problem that AS cannot find the jni headers.
# See https://github.com/googlesamples/android-ndk/issues/319
Expand Down
Loading

0 comments on commit 5d9401a

Please sign in to comment.