diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java index 96426eb71..ac3e97359 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java @@ -148,7 +148,7 @@ public void populateViewHolder(ChatHolder holder, Chat chat, int position) { } @Override - protected void onDataChanged() { + public void onDataChanged() { // If there are no chat messages, show a view that invites the user to add a message. mEmptyListMessage.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); } diff --git a/database/src/androidTest/java/com/firebase/ui/database/Bean.java b/database/src/androidTest/java/com/firebase/ui/database/Bean.java index faf5ccbcb..600497f0b 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/Bean.java +++ b/database/src/androidTest/java/com/firebase/ui/database/Bean.java @@ -30,4 +30,16 @@ public String getText() { public boolean isBool() { return mBool; } + + public void setNumber(int number) { + mNumber = number; + } + + public void setText(String text) { + mText = text; + } + + public void setBool(boolean bool) { + mBool = bool; + } } diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java index 40c521986..4d8c9ea4f 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java @@ -29,7 +29,6 @@ import java.util.concurrent.Callable; import static com.firebase.ui.database.TestUtils.getAppInstance; -import static com.firebase.ui.database.TestUtils.getBean; import static com.firebase.ui.database.TestUtils.runAndWaitUntil; @RunWith(AndroidJUnit4.class) @@ -37,7 +36,8 @@ public class FirebaseArrayOfObjectsTest { private static final int INITIAL_SIZE = 3; private DatabaseReference mRef; - private FirebaseArray mArray; + private FirebaseArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -46,9 +46,9 @@ public void setUp() throws Exception { .getReference() .child("firebasearray") .child("objects"); - mArray = new FirebaseArray(mRef); + mArray = new FirebaseArray<>(mRef, Bean.class); mRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -58,14 +58,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -79,7 +79,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -94,7 +94,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 4; + return mArray.getObject(3).getNumber() == 4; } }); } @@ -109,9 +109,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 3 && mArray.getItem(0) - .getValue(Bean.class) - .getNumber() == 4; + return mArray.getObject(3).getNumber() == 3 + && mArray.getObject(0).getNumber() == 4; } }); } @@ -121,16 +120,47 @@ public void testChangePriorities() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mArray.getItem(2).getRef().setPriority(0.5); + mArray.get(2).getRef().setPriority(0.5); } }, new Callable() { @Override public Boolean call() throws Exception { - return getBean(mArray, 0).getNumber() == 3 - && getBean(mArray, 1).getNumber() == 1 - && getBean(mArray, 2).getNumber() == 2; + return mArray.getObject(0).getNumber() == 3 + && mArray.getObject(1).getNumber() == 1 + && mArray.getObject(2).getNumber() == 2; //return isValuesEqual(mArray, new int[]{3, 1, 2}); } }); } + + @Test + public void testCacheInvalidates() throws Exception { + final DatabaseReference pushRef = mRef.push(); + + // Set initial value to "5" + runAndWaitUntil(mArray, new Runnable() { + @Override + public void run() { + pushRef.setValue(new Bean(5), 100); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + return mArray.getObject(3).getNumber() == 5; + } + }); + + // Change the value to "6" and ensure that the change is propagated + runAndWaitUntil(mArray, new Runnable() { + @Override + public void run() { + pushRef.setValue(new Bean(6), 100); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + return mArray.getObject(3).getNumber() == 6; + } + }); + } } diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java index 2b7c4bf93..adfe0a843 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java @@ -37,6 +37,7 @@ public class FirebaseArrayTest { private static final int INITIAL_SIZE = 3; private DatabaseReference mRef; private FirebaseArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -44,7 +45,7 @@ public void setUp() throws Exception { mRef = FirebaseDatabase.getInstance(app).getReference().child("firebasearray"); mArray = new FirebaseArray(mRef); mRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -54,14 +55,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -75,7 +76,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -90,7 +91,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(4); } }); } @@ -105,8 +106,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(3) - && mArray.getItem(0).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(3) + && mArray.get(0).getValue(Integer.class).equals(4); } }); } @@ -116,7 +117,7 @@ public void testChangePriorityBackToFront() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mArray.getItem(2).getRef().setPriority(0.5); + mArray.get(2).getRef().setPriority(0.5); } }, new Callable() { @Override @@ -131,7 +132,7 @@ public void testChangePriorityFrontToBack() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mArray.getItem(0).getRef().setPriority(4); + mArray.get(0).getRef().setPriority(4); } }, new Callable() { @Override diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java index 959582a74..3377d5c56 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java @@ -28,7 +28,6 @@ import java.util.concurrent.Callable; import static com.firebase.ui.database.TestUtils.getAppInstance; -import static com.firebase.ui.database.TestUtils.getBean; import static com.firebase.ui.database.TestUtils.runAndWaitUntil; @RunWith(AndroidJUnit4.class) @@ -37,7 +36,8 @@ public class FirebaseIndexArrayOfObjectsTest { private DatabaseReference mRef; private DatabaseReference mKeyRef; - private FirebaseArray mArray; + private ObservableSnapshotArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -46,11 +46,11 @@ public void setUp() throws Exception { mRef = databaseInstance.getReference().child("firebasearray").child("objects"); mKeyRef = databaseInstance.getReference().child("firebaseindexarray").child("objects"); - mArray = new FirebaseIndexArray(mKeyRef, mRef); + mArray = new FirebaseIndexArray<>(mKeyRef, mRef, Bean.class); mRef.removeValue(); mKeyRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -60,14 +60,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -81,7 +81,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -96,7 +96,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 4; + return mArray.getObject(3).getNumber() == 4; } }); } @@ -111,9 +111,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 3 && mArray.getItem(0) - .getValue(Bean.class) - .getNumber() == 4; + return mArray.getObject(3).getNumber() == 3 + && mArray.getObject(0).getNumber() == 4; } }); } @@ -123,14 +122,14 @@ public void testChangePriorities() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mKeyRef.child(mArray.getItem(2).getKey()).setPriority(0.5); + mKeyRef.child(mArray.get(2).getKey()).setPriority(0.5); } }, new Callable() { @Override public Boolean call() throws Exception { - return getBean(mArray, 0).getNumber() == 3 - && getBean(mArray, 1).getNumber() == 1 - && getBean(mArray, 2).getNumber() == 2; + return mArray.getObject(0).getNumber() == 3 + && mArray.getObject(1).getNumber() == 1 + && mArray.getObject(2).getNumber() == 2; //return isValuesEqual(mArray, new int[]{3, 1, 2}); } }); diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java index e0b884b3a..493c00b3e 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java @@ -37,7 +37,8 @@ public class FirebaseIndexArrayTest { private DatabaseReference mRef; private DatabaseReference mKeyRef; - private FirebaseIndexArray mArray; + private ObservableSnapshotArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -50,7 +51,7 @@ public void setUp() throws Exception { mRef.removeValue(); mKeyRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -60,14 +61,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -81,7 +82,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -96,7 +97,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(4); } }); } @@ -111,8 +112,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(3) - && mArray.getItem(0).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(3) + && mArray.get(0).getValue(Integer.class).equals(4); } }); } @@ -122,7 +123,7 @@ public void testChangePriorities() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mKeyRef.child(mArray.getItem(2).getKey()).setPriority(0.5); + mKeyRef.child(mArray.get(2).getKey()).setPriority(0.5); } }, new Callable() { @Override diff --git a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java index aa9151fc3..88dc0b693 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java +++ b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java @@ -4,6 +4,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; @@ -33,14 +34,14 @@ private static FirebaseApp initializeApp(Context context) { .build(), APP_NAME); } - public static void runAndWaitUntil(FirebaseArray array, - Runnable task, - Callable done) throws InterruptedException { + public static ChangeEventListener runAndWaitUntil(ObservableSnapshotArray array, + Runnable task, + Callable done) throws InterruptedException { final Semaphore semaphore = new Semaphore(0); - - array.setOnChangedListener(new ChangeEventListener() { + ChangeEventListener listener = array.addChangeEventListener(new ChangeEventListener() { @Override public void onChildChanged(ChangeEventListener.EventType type, + DataSnapshot snapshot, int index, int oldIndex) { semaphore.release(); @@ -56,6 +57,7 @@ public void onCancelled(DatabaseError error) { } }); task.run(); + boolean isDone = false; long startedAt = System.currentTimeMillis(); while (!isDone && System.currentTimeMillis() - startedAt < TIMEOUT) { @@ -68,23 +70,20 @@ public void onCancelled(DatabaseError error) { } } assertTrue("Timed out waiting for expected results on FirebaseArray", isDone); - array.setOnChangedListener(null); + + return listener; } - public static boolean isValuesEqual(FirebaseArray array, int[] expected) { - if (array.getCount() != expected.length) return false; - for (int i = 0; i < array.getCount(); i++) { - if (!array.getItem(i).getValue(Integer.class).equals(expected[i])) { + public static boolean isValuesEqual(ObservableSnapshotArray array, int[] expected) { + if (array.size() != expected.length) return false; + for (int i = 0; i < array.size(); i++) { + if (!array.get(i).getValue(Integer.class).equals(expected[i])) { return false; } } return true; } - public static Bean getBean(FirebaseArray array, int index) { - return array.getItem(index).getValue(Bean.class); - } - public static void pushValue(DatabaseReference keyRef, DatabaseReference ref, Object value, diff --git a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java new file mode 100644 index 000000000..1a564f65e --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java @@ -0,0 +1,64 @@ +package com.firebase.ui.database; + +import android.support.annotation.NonNull; + +import com.google.firebase.database.DataSnapshot; + +import java.util.HashMap; +import java.util.Map; + +/** + * An extension of {@link ObservableSnapshotArray} that caches the result of {@link #getObject(int)} + * so that repeated calls for the same key are not expensive (unless the underlying snapshot has + * changed). + */ +public abstract class CachingObservableSnapshotArray extends ObservableSnapshotArray { + private Map mObjectCache = new HashMap<>(); + + /** + * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) + */ + public CachingObservableSnapshotArray(@NonNull Class tClass) { + super(tClass); + } + + /** + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + */ + public CachingObservableSnapshotArray(@NonNull SnapshotParser parser) { + super(parser); + } + + @Override + public T getObject(int index) { + String key = get(index).getKey(); + + // Return from the cache if possible, otherwise populate the cache and return + if (mObjectCache.containsKey(key)) { + return mObjectCache.get(key); + } else { + T object = super.getObject(index); + mObjectCache.put(key, object); + return object; + } + } + + protected void clearData() { + getSnapshots().clear(); + mObjectCache.clear(); + } + + protected DataSnapshot removeData(int index) { + DataSnapshot snapshot = getSnapshots().remove(index); + if (snapshot != null) { + mObjectCache.remove(snapshot.getKey()); + } + + return snapshot; + } + + protected void updateData(int index, DataSnapshot snapshot) { + getSnapshots().set(index, snapshot); + mObjectCache.remove(snapshot.getKey()); + } +} diff --git a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java index a8028dc47..626773d67 100644 --- a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java +++ b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java @@ -39,19 +39,22 @@ enum EventType { * A callback for when a child has changed in FirebaseArray. * * @param type The type of event received + * @param snapshot the {@link DataSnapshot} of the changed child. * @param index The index at which the change occurred - * @param oldIndex If {@code type} is a moved event, the previous index of the moved child. - * For any other event, {@code oldIndex} will be -1. + * @param oldIndex If {@code type} is a moved event, the previous index of the moved child. For + * any other event, {@code oldIndex} will be -1. */ - void onChildChanged(EventType type, int index, int oldIndex); + void onChildChanged(EventType type, DataSnapshot snapshot, int index, int oldIndex); - /** This method will be triggered each time updates from the database have been completely processed. - * So the first time this method is called, the initial data has been loaded - including the case - * when no data at all is available. Each next time the method is called, a complete update (potentially - * consisting of updates to multiple child items) has been completed. + /** + * This method will be triggered each time updates from the database have been completely + * processed. So the first time this method is called, the initial data has been loaded - + * including the case when no data at all is available. Each next time the method is called, a + * complete update (potentially consisting of updates to multiple child items) has been + * completed. *

- * You would typically override this method to hide a loading indicator (after the initial load) or - * to complete a batch update to a UI element. + * You would typically override this method to hide a loading indicator (after the initial load) + * or to complete a batch update to a UI element. */ void onDataChanged(); diff --git a/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java b/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java new file mode 100644 index 000000000..215a78972 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java @@ -0,0 +1,24 @@ +package com.firebase.ui.database; + +import android.support.annotation.NonNull; + +import com.google.firebase.database.DataSnapshot; + +/** + * A convenience implementation of {@link SnapshotParser} that converts a {@link DataSnapshot} to + * the parametrized class via {@link DataSnapshot#getValue(Class)}. + * + * @param the POJO class to create from snapshots. + */ +public class ClassSnapshotParser implements SnapshotParser { + private Class mClass; + + public ClassSnapshotParser(@NonNull Class clazz) { + mClass = Preconditions.checkNotNull(clazz); + } + + @Override + public T parseSnapshot(DataSnapshot snapshot) { + return snapshot.getValue(mClass); + } +} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseAdapter.java new file mode 100644 index 000000000..977958850 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/FirebaseAdapter.java @@ -0,0 +1,23 @@ +package com.firebase.ui.database; + +import android.support.annotation.RestrictTo; + +import com.google.firebase.database.DatabaseReference; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface FirebaseAdapter extends ChangeEventListener { + /** + * If you need to do some setup before the adapter starts listening for change events in the + * database, do so it here and then call {@code super.startListening()}. + */ + void startListening(); + + /** + * Removes listeners and clears all items in the backing {@link FirebaseArray}. + */ + void cleanup(); + + T getItem(int position); + + DatabaseReference getRef(int position); +} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 68b950c14..e26620372 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -14,6 +14,8 @@ package com.firebase.ui.database; +import android.support.annotation.NonNull; + import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -24,42 +26,70 @@ import java.util.List; /** - * This class implements an array-like collection on top of a Firebase location. + * This class implements a collection on top of a Firebase location. */ -class FirebaseArray implements ChildEventListener, ValueEventListener { +public class FirebaseArray extends CachingObservableSnapshotArray implements ChildEventListener, ValueEventListener { private Query mQuery; - private ChangeEventListener mListener; private List mSnapshots = new ArrayList<>(); - public FirebaseArray(Query ref) { - mQuery = ref; - mQuery.addChildEventListener(this); - mQuery.addValueEventListener(this); + /** + * Create a new FirebaseArray that parses snapshots as members of a given class. + * + * @param query The Firebase location to watch for data changes. Can also be a slice of a + * location, using some combination of {@code limit()}, {@code startAt()}, and + * {@code endAt()}. + * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) + */ + public FirebaseArray(Query query, Class tClass) { + super(tClass); + init(query); } - public void cleanup() { - mQuery.removeEventListener((ValueEventListener) this); - mQuery.removeEventListener((ChildEventListener) this); + /** + * Create a new FirebaseArray with a custom {@link SnapshotParser}. + * + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + * @see FirebaseArray#FirebaseArray(Query, Class) + */ + public FirebaseArray(Query query, SnapshotParser parser) { + super(parser); + init(query); } - public int getCount() { - return mSnapshots.size(); + private void init(Query query) { + mQuery = query; } - public DataSnapshot getItem(int index) { - return mSnapshots.get(index); + @Override + protected List getSnapshots() { + return mSnapshots; } - private int getIndexForKey(String key) { - int index = 0; - for (DataSnapshot snapshot : mSnapshots) { - if (snapshot.getKey().equals(key)) { - return index; - } else { - index++; - } + @Override + public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { + boolean wasListening = isListening(); + super.addChangeEventListener(listener); + + // Only start listening when the first listener is added + if (!wasListening) { + mQuery.addChildEventListener(this); + mQuery.addValueEventListener(this); + } + + return listener; + } + + @Override + public void removeChangeEventListener(@NonNull ChangeEventListener listener) { + super.removeChangeEventListener(listener); + + // Clear data when all listeners are removed + if (!isListening()) { + mQuery.removeEventListener((ValueEventListener) this); + mQuery.removeEventListener((ChildEventListener) this); + + clearData(); } - throw new IllegalArgumentException("Key not found"); } @Override @@ -68,60 +98,87 @@ public void onChildAdded(DataSnapshot snapshot, String previousChildKey) { if (previousChildKey != null) { index = getIndexForKey(previousChildKey) + 1; } + mSnapshots.add(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.ADDED, index); + + notifyChangeEventListeners(ChangeEventListener.EventType.ADDED, snapshot, index); } @Override public void onChildChanged(DataSnapshot snapshot, String previousChildKey) { int index = getIndexForKey(snapshot.getKey()); - mSnapshots.set(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.CHANGED, index); + + updateData(index, snapshot); + notifyChangeEventListeners(ChangeEventListener.EventType.CHANGED, snapshot, index); } @Override public void onChildRemoved(DataSnapshot snapshot) { int index = getIndexForKey(snapshot.getKey()); - mSnapshots.remove(index); - notifyChangedListeners(ChangeEventListener.EventType.REMOVED, index); + + removeData(index); + notifyChangeEventListeners(ChangeEventListener.EventType.REMOVED, snapshot, index); } @Override public void onChildMoved(DataSnapshot snapshot, String previousChildKey) { int oldIndex = getIndexForKey(snapshot.getKey()); mSnapshots.remove(oldIndex); + int newIndex = previousChildKey == null ? 0 : (getIndexForKey(previousChildKey) + 1); mSnapshots.add(newIndex, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.MOVED, newIndex, oldIndex); + + notifyChangeEventListeners(ChangeEventListener.EventType.MOVED, + snapshot, + newIndex, + oldIndex); } @Override public void onDataChange(DataSnapshot dataSnapshot) { - mListener.onDataChanged(); + notifyListenersOnDataChanged(); } @Override public void onCancelled(DatabaseError error) { - notifyCancelledListeners(error); + notifyListenersOnCancelled(error); } - public void setOnChangedListener(ChangeEventListener listener) { - mListener = listener; + private int getIndexForKey(String key) { + int index = 0; + for (DataSnapshot snapshot : mSnapshots) { + if (snapshot.getKey().equals(key)) { + return index; + } else { + index++; + } + } + throw new IllegalArgumentException("Key not found"); } - protected void notifyChangedListeners(ChangeEventListener.EventType type, int index) { - notifyChangedListeners(type, index, -1); + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + FirebaseArray snapshots = (FirebaseArray) obj; + + return mQuery.equals(snapshots.mQuery) && mSnapshots.equals(snapshots.mSnapshots); } - protected void notifyChangedListeners(ChangeEventListener.EventType type, int index, int oldIndex) { - if (mListener != null) { - mListener.onChildChanged(type, index, oldIndex); - } + @Override + public int hashCode() { + int result = mQuery.hashCode(); + result = 31 * result + mSnapshots.hashCode(); + return result; } - protected void notifyCancelledListeners(DatabaseError error) { - if (mListener != null) { - mListener.onCancelled(error); + @Override + public String toString() { + if (isListening()) { + return "FirebaseArray is listening at " + mQuery + ":\n" + mSnapshots; + } else { + return "FirebaseArray is inactive"; } } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index 37077dff5..e7b700cf2 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -14,57 +14,125 @@ package com.firebase.ui.database; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; import android.util.Log; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Query; import com.google.firebase.database.ValueEventListener; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -class FirebaseIndexArray extends FirebaseArray { - private static final String TAG = FirebaseIndexArray.class.getSimpleName(); +public class FirebaseIndexArray extends CachingObservableSnapshotArray implements ChangeEventListener { + private static final String TAG = "FirebaseIndexArray"; - private Query mQuery; - private ChangeEventListener mListener; + private DatabaseReference mDataRef; private Map mRefs = new HashMap<>(); + + private FirebaseArray mKeySnapshots; private List mDataSnapshots = new ArrayList<>(); - public FirebaseIndexArray(Query keyRef, Query dataRef) { - super(keyRef); - mQuery = dataRef; + /** + * Create a new FirebaseIndexArray that parses snapshots as members of a given class. + * + * @param keyQuery The Firebase location containing the list of keys to be found in {@code + * dataRef}. Can also be a slice of a location, using some combination of {@code + * limit()}, {@code startAt()}, and {@code endAt()}. + * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code + * keyQuery}'s location represents a list item in the {@link RecyclerView}. + * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) + */ + public FirebaseIndexArray(Query keyQuery, DatabaseReference dataRef, Class tClass) { + super(tClass); + init(keyQuery, dataRef); + } + + /** + * Create a new FirebaseIndexArray with a custom {@link SnapshotParser}. + * + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + * @see FirebaseIndexArray#FirebaseIndexArray(Query, DatabaseReference, Class) + */ + public FirebaseIndexArray(Query keyQuery, DatabaseReference dataRef, SnapshotParser parser) { + super(parser); + init(keyQuery, dataRef); + } + + private void init(Query keyQuery, DatabaseReference dataRef) { + mDataRef = dataRef; + mKeySnapshots = new FirebaseArray<>(keyQuery, new SnapshotParser() { + @Override + public String parseSnapshot(DataSnapshot snapshot) { + return snapshot.getKey(); + } + }); + + mKeySnapshots.addChangeEventListener(this); } @Override - public void cleanup() { - super.cleanup(); - Set refs = new HashSet<>(mRefs.keySet()); - for (Query ref : refs) { - ref.removeEventListener(mRefs.remove(ref)); + public void onChildChanged(EventType type, DataSnapshot snapshot, int index, int oldIndex) { + switch (type) { + case ADDED: + onKeyAdded(snapshot); + break; + case MOVED: + onKeyMoved(snapshot, index, oldIndex); + break; + case CHANGED: + // This is a no-op, we don't care when a key 'changes' since that should not + // be a supported operation + break; + case REMOVED: + onKeyRemoved(snapshot, index); + break; } } @Override - public int getCount() { - return mDataSnapshots.size(); + public void onDataChanged() { + // No-op, we don't listen to batch events for the key ref } @Override - public DataSnapshot getItem(int index) { - return mDataSnapshots.get(index); + public void onCancelled(DatabaseError error) { + Log.e(TAG, "A fatal error occurred retrieving the necessary keys to populate your adapter."); + } + + @Override + public void removeChangeEventListener(@NonNull ChangeEventListener listener) { + super.removeChangeEventListener(listener); + if (!isListening()) { + for (Query query : mRefs.keySet()) { + query.removeEventListener(mRefs.get(query)); + } + + clearData(); + } + } + + @Override + protected List getSnapshots() { + return mDataSnapshots; + } + + @Override + protected void clearData() { + super.clearData(); + mRefs.clear(); } private int getIndexForKey(String key) { - int dataCount = getCount(); + int dataCount = size(); int index = 0; for (int keyIndex = 0; index < dataCount; keyIndex++) { - String superKey = super.getItem(keyIndex).getKey(); + String superKey = mKeySnapshots.getObject(keyIndex); if (key.equals(superKey)) { break; } else if (mDataSnapshots.get(index).getKey().equals(superKey)) { @@ -74,73 +142,75 @@ private int getIndexForKey(String key) { return index; } + /** + * Determines if a DataSnapshot with the given key is present at the given index. + */ private boolean isKeyAtIndex(String key, int index) { - return index >= 0 && index < getCount() && mDataSnapshots.get(index).getKey().equals(key); + return index >= 0 && index < size() && mDataSnapshots.get(index).getKey().equals(key); } - @Override - public void onChildAdded(DataSnapshot keySnapshot, String previousChildKey) { - super.setOnChangedListener(null); - super.onChildAdded(keySnapshot, previousChildKey); - super.setOnChangedListener(mListener); + protected void onKeyAdded(DataSnapshot data) { + Query ref = mDataRef.child(data.getKey()); - Query ref = mQuery.getRef().child(keySnapshot.getKey()); + // Start listening mRefs.put(ref, ref.addValueEventListener(new DataRefListener())); } - @Override - public void onChildChanged(DataSnapshot snapshot, String previousChildKey) { - super.setOnChangedListener(null); - super.onChildChanged(snapshot, previousChildKey); - super.setOnChangedListener(mListener); - } + protected void onKeyMoved(DataSnapshot data, int index, int oldIndex) { + String key = data.getKey(); - @Override - public void onChildRemoved(DataSnapshot keySnapshot) { - String key = keySnapshot.getKey(); - int index = getIndexForKey(key); - mQuery.getRef().child(key).removeEventListener(mRefs.remove(mQuery.getRef().child(key))); + if (isKeyAtIndex(key, oldIndex)) { + DataSnapshot snapshot = removeData(oldIndex); + mDataSnapshots.add(index, snapshot); + notifyChangeEventListeners(ChangeEventListener.EventType.MOVED, + snapshot, + index, + oldIndex); + } + } - super.setOnChangedListener(null); - super.onChildRemoved(keySnapshot); - super.setOnChangedListener(mListener); + protected void onKeyRemoved(DataSnapshot data, int index) { + String key = data.getKey(); + mDataRef.child(key).removeEventListener(mRefs.remove(mDataRef.getRef().child(key))); if (isKeyAtIndex(key, index)) { - mDataSnapshots.remove(index); - notifyChangedListeners(ChangeEventListener.EventType.REMOVED, index); + DataSnapshot snapshot = removeData(index); + notifyChangeEventListeners(ChangeEventListener.EventType.REMOVED, snapshot, index); } } @Override - public void onChildMoved(DataSnapshot keySnapshot, String previousChildKey) { - String key = keySnapshot.getKey(); - int oldIndex = getIndexForKey(key); + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + if (!super.equals(obj)) return false; - super.setOnChangedListener(null); - super.onChildMoved(keySnapshot, previousChildKey); - super.setOnChangedListener(mListener); + FirebaseIndexArray array = (FirebaseIndexArray) obj; - if (isKeyAtIndex(key, oldIndex)) { - DataSnapshot snapshot = mDataSnapshots.remove(oldIndex); - int newIndex = getIndexForKey(key); - mDataSnapshots.add(newIndex, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.MOVED, newIndex, oldIndex); - } + return mDataRef.equals(array.mDataRef) && mDataSnapshots.equals(array.mDataSnapshots); } @Override - public void onCancelled(DatabaseError error) { - Log.e(TAG, "A fatal error occurred retrieving the necessary keys to populate your adapter."); - super.onCancelled(error); + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + mDataRef.hashCode(); + result = 31 * result + mDataSnapshots.hashCode(); + return result; } @Override - public void setOnChangedListener(ChangeEventListener listener) { - super.setOnChangedListener(listener); - mListener = listener; + public String toString() { + if (isListening()) { + return "FirebaseIndexArray is listening at " + mDataRef + ":\n" + mDataSnapshots; + } else { + return "FirebaseIndexArray is inactive"; + } } - private class DataRefListener implements ValueEventListener { + /** + * A ValueEventListener attached to the joined child data. + */ + protected class DataRefListener implements ValueEventListener { @Override public void onDataChange(DataSnapshot snapshot) { String key = snapshot.getKey(); @@ -148,17 +218,21 @@ public void onDataChange(DataSnapshot snapshot) { if (snapshot.getValue() != null) { if (!isKeyAtIndex(key, index)) { + // We don't already know about this data, add it mDataSnapshots.add(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.ADDED, index); + notifyChangeEventListeners(ChangeEventListener.EventType.ADDED, snapshot, index); } else { - mDataSnapshots.set(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.CHANGED, index); + // We already know about this data, just update it + updateData(index, snapshot); + notifyChangeEventListeners(ChangeEventListener.EventType.CHANGED, snapshot, index); } } else { if (isKeyAtIndex(key, index)) { - mDataSnapshots.remove(index); - notifyChangedListeners(ChangeEventListener.EventType.REMOVED, index); + // This data has disappeared, remove it + removeData(index); + notifyChangeEventListeners(ChangeEventListener.EventType.REMOVED, snapshot, index); } else { + // Data does not exist Log.w(TAG, "Key not found at ref: " + snapshot.getRef()); } } @@ -166,7 +240,7 @@ public void onDataChange(DataSnapshot snapshot) { @Override public void onCancelled(DatabaseError error) { - notifyCancelledListeners(error); + notifyListenersOnCancelled(error); } } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java index a5878d632..06799a8ba 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java @@ -2,58 +2,40 @@ import android.app.Activity; import android.support.annotation.LayoutRes; +import android.widget.ListView; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Query; -/** - * This class is a generic way of backing an Android ListView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. Extend this class and provide an implementation of {@code populateView}, which will be given an - * instance of your list item mLayout and an instance your class that holds your data. - * Simply populate the view however you like and this class will handle updating the list as the data changes. - *

- * If your data is not indexed: - *

- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *     ListAdapter adapter = new FirebaseListAdapter(
- *             this,
- *             ChatMessage.class,
- *             android.R.layout.two_line_list_item,
- *             keyRef,
- *             dataRef)
- *     {
- *         protected void populateView(View view, ChatMessage chatMessage, int position)
- *         {
- *             ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName());
- *             ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getMessage());
- *         }
- *     };
- *     listView.setListAdapter(adapter);
- * 
- * - * @param The class type to use as a model for the data - * contained in the children of the given Firebase location - */ public abstract class FirebaseIndexListAdapter extends FirebaseListAdapter { /** - * @param activity The activity containing the ListView - * @param modelClass Firebase will marshall the data at a location into - * an instance of a class that you provide - * @param modelLayout This is the layout used to represent a single list item. - * You will be responsible for populating an instance of the corresponding - * view with the data from an instance of modelClass. - * @param keyRef The Firebase location containing the list of keys to be found in {@code dataRef}. - * Can also be a slice of a location, using some - * combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. - * @param dataRef The Firebase location to watch for data changes. - * Each key key found in {@code keyRef}'s location represents - * a list item in the {@code ListView}. + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the + * model class + * @param keyQuery The Firebase location containing the list of keys to be found in {@code + * dataRef}. Can also be a slice of a location, using some combination of {@code + * limit()}, {@code startAt()}, and {@code endAt()}. + * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code + * keyQuery}'s location represents a list item in the {@link ListView}. + * @see FirebaseIndexListAdapter#FirebaseIndexListAdapter(Activity, SnapshotParser, int, Query, + * DatabaseReference) + */ + public FirebaseIndexListAdapter(Activity activity, + SnapshotParser parser, + @LayoutRes int modelLayout, + Query keyQuery, + DatabaseReference dataRef) { + super(activity, new FirebaseIndexArray<>(keyQuery, dataRef, parser), modelLayout); + } + + /** + * @see #FirebaseIndexListAdapter(Activity, SnapshotParser, int, Query, DatabaseReference) */ public FirebaseIndexListAdapter(Activity activity, Class modelClass, @LayoutRes int modelLayout, - Query keyRef, - Query dataRef) { - super(activity, modelClass, modelLayout, new FirebaseIndexArray(keyRef, dataRef)); + Query keyQuery, + DatabaseReference dataRef) { + this(activity, new ClassSnapshotParser<>(modelClass), modelLayout, keyQuery, dataRef); } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java index 19e45d401..b2577e44f 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java @@ -1,87 +1,44 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * 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 com.firebase.ui.database; import android.support.annotation.LayoutRes; import android.support.v7.widget.RecyclerView; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Query; -/** - * This class is a generic way of backing an RecyclerView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. - *

- * To use this class in your app, subclass it passing in all required parameters and implement the - * populateViewHolder method. - *

- *

- *     private static class ChatMessageViewHolder extends RecyclerView.ViewHolder {
- *         TextView messageText;
- *         TextView nameText;
- *
- *         public ChatMessageViewHolder(View itemView) {
- *             super(itemView);
- *             nameText = (TextView)itemView.findViewById(android.R.id.text1);
- *             messageText = (TextView) itemView.findViewById(android.R.id.text2);
- *         }
- *     }
- *
- *     FirebaseIndexRecyclerAdapter adapter;
- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *
- *     RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler);
- *     recycler.setHasFixedSize(true);
- *     recycler.setLayoutManager(new LinearLayoutManager(this));
- *
- *     adapter = new FirebaseIndexRecyclerAdapter(
- *          ChatMessage.class, android.R.layout.two_line_list_item, ChatMessageViewHolder.class, keyRef, dataRef) {
- *         public void populateViewHolder(ChatMessageViewHolder chatMessageViewHolder,
- *                                        ChatMessage chatMessage,
- *                                        int position) {
- *             chatMessageViewHolder.nameText.setText(chatMessage.getName());
- *             chatMessageViewHolder.messageText.setText(chatMessage.getMessage());
- *         }
- *     };
- *     recycler.setAdapter(mAdapter);
- * 
- * - * @param The Java class that maps to the type of objects stored in the Firebase location. - * @param The ViewHolder class that contains the Views in the layout that is shown for each object. - */ public abstract class FirebaseIndexRecyclerAdapter extends FirebaseRecyclerAdapter { /** - * @param modelClass Firebase will marshall the data at a location into an instance - * of a class that you provide - * @param modelLayout This is the layout used to represent a single item in the list. - * You will be responsible for populating an - * instance of the corresponding view with the data from an instance of modelClass. - * @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout. - * @param keyRef The Firebase location containing the list of keys to be found in {@code dataRef}. - * Can also be a slice of a location, using some - * combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. - * @param dataRef The Firebase location to watch for data changes. - * Each key key found at {@code keyRef}'s location represents - * a list item in the {@code RecyclerView}. + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the + * model class + * @param keyQuery The Firebase location containing the list of keys to be found in {@code + * dataRef}. Can also be a slice of a location, using some combination of {@code + * limit()}, {@code startAt()}, and {@code endAt()}. + * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code + * keyQuery}'s location represents a list item in the {@link RecyclerView}. + * @see FirebaseRecyclerAdapter#FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class) + */ + public FirebaseIndexRecyclerAdapter(SnapshotParser parser, + @LayoutRes int modelLayout, + Class viewHolderClass, + Query keyQuery, + DatabaseReference dataRef) { + super(new FirebaseIndexArray<>(keyQuery, dataRef, parser), modelLayout, viewHolderClass); + } + + /** + * @see #FirebaseIndexRecyclerAdapter(SnapshotParser, int, Class, Query, DatabaseReference) */ public FirebaseIndexRecyclerAdapter(Class modelClass, @LayoutRes int modelLayout, Class viewHolderClass, - Query keyRef, - Query dataRef) { - super(modelClass, modelLayout, viewHolderClass, new FirebaseIndexArray(keyRef, dataRef)); + Query keyQuery, + DatabaseReference dataRef) { + this(new ClassSnapshotParser<>(modelClass), + modelLayout, + viewHolderClass, + keyQuery, + dataRef); } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java index 5ae25c451..acb781a0d 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java @@ -1,17 +1,3 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * 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 com.firebase.ui.database; import android.app.Activity; @@ -20,6 +6,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.ListView; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -27,114 +14,113 @@ import com.google.firebase.database.Query; /** - * This class is a generic way of backing an Android ListView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. Extend this class and provide an implementation of {@code populateView}, which will be given an - * instance of your list item mLayout and an instance your class that holds your data. - * Simply populate the view however you like and this class will handle updating the list as the data changes. + * This class is a generic way of backing an Android {@link android.widget.ListView} with a Firebase + * location. It handles all of the child events at the given Firebase location. It marshals received + * data into the given class type. *

- *

- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *     ListAdapter adapter = new FirebaseListAdapter(
- *              this, ChatMessage.class, android.R.layout.two_line_list_item, ref)
- *     {
- *         protected void populateView(View view, ChatMessage chatMessage, int position)
- *         {
- *             ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName());
- *             ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getMessage());
- *         }
- *     };
- *     listView.setListAdapter(adapter);
- * 
+ * See the README + * for an in-depth tutorial on how to set up the FirebaseListAdapter. * - * @param The class type to use as a model for the data - * contained in the children of the given Firebase location + * @param The class type to use as a model for the data contained in the children of the given + * Firebase location */ -public abstract class FirebaseListAdapter extends BaseAdapter { +public abstract class FirebaseListAdapter extends BaseAdapter implements FirebaseAdapter { private static final String TAG = "FirebaseListAdapter"; - private FirebaseArray mSnapshots; - private final Class mModelClass; protected Activity mActivity; + protected ObservableSnapshotArray mSnapshots; protected int mLayout; - FirebaseListAdapter(Activity activity, - Class modelClass, - @LayoutRes int modelLayout, - FirebaseArray snapshots) { + /** + * @param activity The {@link Activity} containing the {@link ListView} + * @param modelLayout This is the layout used to represent a single list item. You will be + * responsible for populating an instance of the corresponding view with the + * data from an instance of modelClass. + * @param snapshots The data used to populate the adapter + */ + public FirebaseListAdapter(Activity activity, + ObservableSnapshotArray snapshots, + @LayoutRes int modelLayout) { mActivity = activity; - mModelClass = modelClass; - mLayout = modelLayout; mSnapshots = snapshots; + mLayout = modelLayout; - mSnapshots.setOnChangedListener(new ChangeEventListener() { - @Override - public void onChildChanged(EventType type, int index, int oldIndex) { - FirebaseListAdapter.this.onChildChanged(type, index, oldIndex); - } - - @Override - public void onDataChanged() { - FirebaseListAdapter.this.onDataChanged(); - } - - @Override - public void onCancelled(DatabaseError error) { - FirebaseListAdapter.this.onCancelled(error); - } - }); + startListening(); + } + + /** + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the model + * class + * @param query The Firebase location to watch for data changes. Can also be a slice of a + * location, using some combination of {@code limit()}, {@code startAt()}, and + * {@code endAt()}. + * @see #FirebaseListAdapter(Activity, ObservableSnapshotArray, int) + */ + public FirebaseListAdapter(Activity activity, + SnapshotParser parser, + @LayoutRes int modelLayout, + Query query) { + this(activity, new FirebaseArray<>(query, parser), modelLayout); } /** - * @param activity The activity containing the ListView - * @param modelClass Firebase will marshall the data at a location into - * an instance of a class that you provide - * @param modelLayout This is the layout used to represent a single list item. - * You will be responsible for populating an instance of the corresponding - * view with the data from an instance of modelClass. - * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, - * using some combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. + * @see #FirebaseListAdapter(Activity, SnapshotParser, int, Query) */ public FirebaseListAdapter(Activity activity, Class modelClass, - int modelLayout, - Query ref) { - this(activity, modelClass, modelLayout, new FirebaseArray(ref)); + @LayoutRes int modelLayout, + Query query) { + this(activity, new ClassSnapshotParser<>(modelClass), modelLayout, query); } + @Override + public void startListening() { + if (!mSnapshots.isListening(this)) { + mSnapshots.addChangeEventListener(this); + } + } + + @Override public void cleanup() { - mSnapshots.cleanup(); + mSnapshots.removeChangeEventListener(this); } @Override - public int getCount() { - return mSnapshots.getCount(); + public void onChildChanged(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + notifyDataSetChanged(); } @Override - public T getItem(int position) { - return parseSnapshot(mSnapshots.getItem(position)); + public void onDataChanged() { } - /** - * This method parses the DataSnapshot into the requested type. You can override it in subclasses - * to do custom parsing. - * - * @param snapshot the DataSnapshot to extract the model from - * @return the model extracted from the DataSnapshot - */ - protected T parseSnapshot(DataSnapshot snapshot) { - return snapshot.getValue(mModelClass); + @Override + public void onCancelled(DatabaseError error) { + Log.w(TAG, error.toException()); } + @Override + public T getItem(int position) { + return mSnapshots.getObject(position); + } + + @Override public DatabaseReference getRef(int position) { - return mSnapshots.getItem(position).getRef(); + return mSnapshots.get(position).getRef(); + } + + @Override + public int getCount() { + return mSnapshots.size(); } @Override public long getItemId(int i) { // http://stackoverflow.com/questions/5100071/whats-the-purpose-of-item-ids-in-android-listview-adapter - return mSnapshots.getItem(i).getKey().hashCode(); + return mSnapshots.get(i).getKey().hashCode(); } @Override @@ -150,26 +136,6 @@ public View getView(int position, View view, ViewGroup viewGroup) { return view; } - /** - * @see ChangeEventListener#onChildChanged(ChangeEventListener.EventType, int, int) - */ - protected void onChildChanged(ChangeEventListener.EventType type, int index, int oldIndex) { - notifyDataSetChanged(); - } - - /** - * @see ChangeEventListener#onDataChanged() - */ - protected void onDataChanged() { - } - - /** - * @see ChangeEventListener#onCancelled(DatabaseError) - */ - protected void onCancelled(DatabaseError error) { - Log.w(TAG, error.toException()); - } - /** * Each time the data at the given Firebase location changes, * this method will be called for each item that needs to be displayed. diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java index 834f0e01e..3cf86d878 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java @@ -1,17 +1,3 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * 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 com.firebase.ui.database; import android.support.annotation.LayoutRes; @@ -30,135 +16,125 @@ import java.lang.reflect.InvocationTargetException; /** - * This class is a generic way of backing an RecyclerView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. - *

- * To use this class in your app, subclass it passing in all required parameters and implement the - * populateViewHolder method. - *

- *

- *     private static class ChatMessageViewHolder extends RecyclerView.ViewHolder {
- *         TextView messageText;
- *         TextView nameText;
- *
- *         public ChatMessageViewHolder(View itemView) {
- *             super(itemView);
- *             nameText = (TextView)itemView.findViewById(android.R.id.text1);
- *             messageText = (TextView) itemView.findViewById(android.R.id.text2);
- *         }
- *     }
- *
- *     FirebaseRecyclerAdapter adapter;
- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *
- *     RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler);
- *     recycler.setHasFixedSize(true);
- *     recycler.setLayoutManager(new LinearLayoutManager(this));
- *
- *     adapter = new FirebaseRecyclerAdapter(
- *           ChatMessage.class, android.R.layout.two_line_list_item, ChatMessageViewHolder.class, ref) {
- *         public void populateViewHolder(ChatMessageViewHolder chatMessageViewHolder,
- *                                        ChatMessage chatMessage,
- *                                        int position) {
- *             chatMessageViewHolder.nameText.setText(chatMessage.getName());
- *             chatMessageViewHolder.messageText.setText(chatMessage.getMessage());
- *         }
- *     };
- *     recycler.setAdapter(mAdapter);
- * 
- *

- * To avoid Context leaks, make sure you invoke {@link #cleanup() cleanup} + * This class is a generic way of backing a {@link RecyclerView} with a Firebase location. It + * handles all of the child events at the given Firebase location and marshals received data into + * the given class type. *

+ * See the README + * for an in-depth tutorial on how to set up the FirebaseRecyclerAdapter. * * @param The Java class that maps to the type of objects stored in the Firebase location. - * @param The ViewHolder class that contains the Views in the layout that is shown for each object. + * @param The {@link RecyclerView.ViewHolder} class that contains the Views in the layout that + * is shown for each object. */ public abstract class FirebaseRecyclerAdapter - extends RecyclerView.Adapter { + extends RecyclerView.Adapter implements FirebaseAdapter { private static final String TAG = "FirebaseRecyclerAdapter"; - private FirebaseArray mSnapshots; - private Class mModelClass; + protected ObservableSnapshotArray mSnapshots; protected Class mViewHolderClass; protected int mModelLayout; - FirebaseRecyclerAdapter(Class modelClass, - @LayoutRes int modelLayout, - Class viewHolderClass, - FirebaseArray snapshots) { - mModelClass = modelClass; - mModelLayout = modelLayout; - mViewHolderClass = viewHolderClass; + /** + * @param snapshots The data used to populate the adapter + * @param modelLayout This is the layout used to represent a single item in the list. You + * will be responsible for populating an instance of the corresponding + * view with the data from an instance of modelClass. + * @param viewHolderClass The class that hold references to all sub-views in an instance + * modelLayout. + */ + public FirebaseRecyclerAdapter(ObservableSnapshotArray snapshots, + @LayoutRes int modelLayout, + Class viewHolderClass) { mSnapshots = snapshots; + mViewHolderClass = viewHolderClass; + mModelLayout = modelLayout; - mSnapshots.setOnChangedListener(new ChangeEventListener() { - @Override - public void onChildChanged(EventType type, int index, int oldIndex) { - FirebaseRecyclerAdapter.this.onChildChanged(type, index, oldIndex); - } - - @Override - public void onDataChanged() { - FirebaseRecyclerAdapter.this.onDataChanged(); - } - - @Override - public void onCancelled(DatabaseError error) { - FirebaseRecyclerAdapter.this.onCancelled(error); - } - }); + startListening(); } /** - * @param modelClass Firebase will marshall the data at a location into - * an instance of a class that you provide - * @param modelLayout This is the layout used to represent a single item in the list. - * You will be responsible for populating an instance of the corresponding - * view with the data from an instance of modelClass. - * @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout. - * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, - * using some combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the model + * class + * @param query The Firebase location to watch for data changes. Can also be a slice of a + * location, using some combination of {@code limit()}, {@code startAt()}, and + * {@code endAt()}. + * @see #FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class) + */ + public FirebaseRecyclerAdapter(SnapshotParser parser, + @LayoutRes int modelLayout, + Class viewHolderClass, + Query query) { + this(new FirebaseArray<>(query, parser), modelLayout, viewHolderClass); + } + + /** + * @see #FirebaseRecyclerAdapter(SnapshotParser, int, Class, Query) */ public FirebaseRecyclerAdapter(Class modelClass, - int modelLayout, + @LayoutRes int modelLayout, Class viewHolderClass, - Query ref) { - this(modelClass, modelLayout, viewHolderClass, new FirebaseArray(ref)); + Query query) { + this(new ClassSnapshotParser<>(modelClass), modelLayout, viewHolderClass, query); } + @Override + public void startListening() { + if (!mSnapshots.isListening(this)) { + mSnapshots.addChangeEventListener(this); + } + } + + @Override public void cleanup() { - mSnapshots.cleanup(); + mSnapshots.removeChangeEventListener(this); } @Override - public int getItemCount() { - return mSnapshots.getCount(); + public void onChildChanged(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + switch (type) { + case ADDED: + notifyItemInserted(index); + break; + case CHANGED: + notifyItemChanged(index); + break; + case REMOVED: + notifyItemRemoved(index); + break; + case MOVED: + notifyItemMoved(oldIndex, index); + break; + default: + throw new IllegalStateException("Incomplete case statement"); + } } - public T getItem(int position) { - return parseSnapshot(mSnapshots.getItem(position)); + @Override + public void onDataChanged() { } - /** - * This method parses the DataSnapshot into the requested type. You can override it in subclasses - * to do custom parsing. - * - * @param snapshot the DataSnapshot to extract the model from - * @return the model extracted from the DataSnapshot - */ - protected T parseSnapshot(DataSnapshot snapshot) { - return snapshot.getValue(mModelClass); + @Override + public void onCancelled(DatabaseError error) { + Log.w(TAG, error.toException()); + } + + @Override + public T getItem(int position) { + return mSnapshots.getObject(position); } + @Override public DatabaseReference getRef(int position) { - return mSnapshots.getItem(position).getRef(); + return mSnapshots.get(position).getRef(); } @Override - public long getItemId(int position) { - // http://stackoverflow.com/questions/5100071/whats-the-purpose-of-item-ids-in-android-listview-adapter - return mSnapshots.getItem(position).getKey().hashCode(); + public int getItemCount() { + return mSnapshots.size(); } @Override @@ -178,50 +154,15 @@ public VH onCreateViewHolder(ViewGroup parent, int viewType) { } } - @Override - public void onBindViewHolder(VH viewHolder, int position) { - T model = getItem(position); - populateViewHolder(viewHolder, model, position); - } - @Override public int getItemViewType(int position) { return mModelLayout; } - /** - * @see ChangeEventListener#onChildChanged(ChangeEventListener.EventType, int, int) - */ - protected void onChildChanged(ChangeEventListener.EventType type, int index, int oldIndex) { - switch (type) { - case ADDED: - notifyItemInserted(index); - break; - case CHANGED: - notifyItemChanged(index); - break; - case REMOVED: - notifyItemRemoved(index); - break; - case MOVED: - notifyItemMoved(oldIndex, index); - break; - default: - throw new IllegalStateException("Incomplete case statement"); - } - } - - /** - * @see ChangeEventListener#onDataChanged() - */ - protected void onDataChanged() { - } - - /** - * @see ChangeEventListener#onCancelled(DatabaseError) - */ - protected void onCancelled(DatabaseError error) { - Log.w(TAG, error.toException()); + @Override + public void onBindViewHolder(VH viewHolder, int position) { + T model = getItem(position); + populateViewHolder(viewHolder, model, position); } /** diff --git a/database/src/main/java/com/firebase/ui/database/ImmutableList.java b/database/src/main/java/com/firebase/ui/database/ImmutableList.java new file mode 100644 index 000000000..103174d0a --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/ImmutableList.java @@ -0,0 +1,202 @@ +package com.firebase.ui.database; + +import android.support.annotation.RestrictTo; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +public abstract class ImmutableList implements List { + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean add(E element) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final void clear() { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final E set(int index, E element) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final void add(int index, E element) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final E remove(int index) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + protected final class ImmutableIterator implements Iterator { + protected Iterator mIterator; + + public ImmutableIterator(Iterator iterator) { + mIterator = iterator; + } + + @Override + public boolean hasNext() { + return mIterator.hasNext(); + } + + @Override + public E next() { + return mIterator.next(); + } + } + + protected final class ImmutableListIterator implements ListIterator { + protected ListIterator mListIterator; + + public ImmutableListIterator(ListIterator listIterator) { + mListIterator = listIterator; + } + + @Override + public boolean hasNext() { + return mListIterator.hasNext(); + } + + @Override + public E next() { + return mListIterator.next(); + } + + @Override + public boolean hasPrevious() { + return mListIterator.hasPrevious(); + } + + @Override + public E previous() { + return mListIterator.previous(); + } + + @Override + public int nextIndex() { + return mListIterator.nextIndex(); + } + + @Override + public int previousIndex() { + return mListIterator.previousIndex(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void set(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(E e) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java new file mode 100644 index 000000000..2918103d6 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -0,0 +1,202 @@ +package com.firebase.ui.database; + +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.support.annotation.RestrictTo; + +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Exposes a collection of items in Firebase as a {@link List} of {@link DataSnapshot}. To observe + * the list attach a {@link com.google.firebase.database.ChildEventListener}. + * + * @param a POJO class to which the DataSnapshots can be converted. + */ +public abstract class ObservableSnapshotArray extends ImmutableList { + protected final List mListeners = new CopyOnWriteArrayList<>(); + protected final SnapshotParser mParser; + + /** + * Create an ObservableSnapshotArray where snapshots are parsed as objects of a particular + * class. + * + * @param clazz the class as which DataSnapshots should be parsed. + * @see ClassSnapshotParser + */ + public ObservableSnapshotArray(@NonNull Class clazz) { + this(new ClassSnapshotParser<>(clazz)); + } + + /** + * Create an ObservableSnapshotArray with a custom {@link SnapshotParser}. + * + * @param parser the {@link SnapshotParser} to use + */ + public ObservableSnapshotArray(@NonNull SnapshotParser parser) { + mParser = Preconditions.checkNotNull(parser); + } + + /** + * Attach a {@link ChangeEventListener} to this array. The listener will receive one {@link + * ChangeEventListener.EventType#ADDED} event for each item that already exists in the array at + * the time of attachment, and then receive all future child events. + */ + @CallSuper + public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { + Preconditions.checkNotNull(listener); + + mListeners.add(listener); + for (int i = 0; i < size(); i++) { + listener.onChildChanged(ChangeEventListener.EventType.ADDED, get(i), i, -1); + } + + return listener; + } + + /** + * Detach a {@link com.google.firebase.database.ChildEventListener} from this array. + */ + @CallSuper + public void removeChangeEventListener(@NonNull ChangeEventListener listener) { + mListeners.remove(listener); + } + + /** + * Removes all {@link ChangeEventListener}s. The list will be empty after this call returns. + * + * @see #removeChangeEventListener(ChangeEventListener) + */ + @CallSuper + public void removeAllListeners() { + for (ChangeEventListener listener : mListeners) { + removeChangeEventListener(listener); + } + } + + protected abstract List getSnapshots(); + + protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index) { + notifyChangeEventListeners(type, snapshot, index, -1); + } + + protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + for (ChangeEventListener listener : mListeners) { + listener.onChildChanged(type, snapshot, index, oldIndex); + } + } + + protected final void notifyListenersOnDataChanged() { + for (ChangeEventListener listener : mListeners) { + listener.onDataChanged(); + } + } + + protected final void notifyListenersOnCancelled(DatabaseError error) { + for (ChangeEventListener listener : mListeners) { + listener.onCancelled(error); + } + } + + /** + * @return true if {@link FirebaseArray} is listening for change events from the Firebase + * database, false otherwise + */ + public final boolean isListening() { + return !mListeners.isEmpty(); + } + + /** + * @return true if the provided {@link ChangeEventListener} is listening for changes + */ + public final boolean isListening(ChangeEventListener listener) { + return mListeners.contains(listener); + } + + /** + * Get the {@link DataSnapshot} at a given position converted to an object of the parametrized + * type. This uses the {@link SnapshotParser} passed to the constructor. If the parser was not + * initialized this will throw an unchecked exception. + */ + public E getObject(int index) { + return mParser.parseSnapshot(get(index)); + } + + @Override + public int size() { + return getSnapshots().size(); + } + + @Override + public boolean isEmpty() { + return getSnapshots().isEmpty(); + } + + @Override + public boolean contains(Object o) { + return getSnapshots().contains(o); + } + + @Override + public Iterator iterator() { + return new ImmutableIterator(getSnapshots().iterator()); + } + + @Override + public DataSnapshot[] toArray() { + return getSnapshots().toArray(new DataSnapshot[getSnapshots().size()]); + } + + @Override + public boolean containsAll(Collection c) { + return getSnapshots().containsAll(c); + } + + @Override + public DataSnapshot get(int index) { + return getSnapshots().get(index); + } + + @Override + public int indexOf(Object o) { + return getSnapshots().indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return getSnapshots().lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new ImmutableListIterator(getSnapshots().listIterator()); + } + + @Override + public ListIterator listIterator(int index) { + return new ImmutableListIterator(getSnapshots().listIterator(index)); + } + + /** + * Guaranteed to throw an exception. Use {@link #toArray()} instead to get an array of {@link + * DataSnapshot}s. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final T[] toArray(T[] a) { + throw new UnsupportedOperationException(); + } +} diff --git a/database/src/main/java/com/firebase/ui/database/Preconditions.java b/database/src/main/java/com/firebase/ui/database/Preconditions.java new file mode 100644 index 000000000..0fb15b2fa --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/Preconditions.java @@ -0,0 +1,14 @@ +package com.firebase.ui.database; + +import android.support.annotation.RestrictTo; + +/** + * Convenience class for checking argument conditions. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class Preconditions { + public static T checkNotNull(T o) { + if (o == null) throw new IllegalArgumentException("Argument cannot be null."); + return o; + } +} diff --git a/database/src/main/java/com/firebase/ui/database/SnapshotParser.java b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java new file mode 100644 index 000000000..8801f54f9 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java @@ -0,0 +1,13 @@ +package com.firebase.ui.database; + +import com.google.firebase.database.DataSnapshot; + +public interface SnapshotParser { + /** + * This method parses the DataSnapshot into the requested type. + * + * @param snapshot the DataSnapshot to extract the model from + * @return the model extracted from the DataSnapshot + */ + T parseSnapshot(DataSnapshot snapshot); +}