TL; DR
I combined ObservableArrayMap
and ChildEventListener
into a FirebaseArrayMap
.
A working FirebaseUI-Android example is available here. Now all you have to do is:
// Remote updates immediately send you your view
map.addOnMapChangedCallback(mUserChangedCallback);
// Non blocking operation to update database
map.create(user);
// Local up-to-date cache
User user = map.get(primaryKey);
Remember to (un)register your OnMapChangedCallback
in onResume
and onPause
in order to avoid memory leaking caused by ChildEventListener
remote updates.
CRUD
First we need to create a thesaurus interface for Map
, i.e.:
public interface CRUD<K, V> {
Task<V> create(V value);
Task<V> create(K key, V value);
Task<Void> createAll(SimpleArrayMap<? extends K, ? extends V> array);
Task<Void> createAll(Map<? extends K, ? extends V> map);
V read(K key);
Task<V> delete(K key);
Task<Void> free();
}
This interface will be implemented by FirebaseArrayMap
, returning the same values that would be returned by Map
.
The methods are analogous to put
, putAll
and get
, but instead of return values they return Tasks
with those values (or exceptions). Example:
@Override
public Task<Void> createAll(Map<? extends K, ? extends V> map) {
Collection<Task<V>> tasks = new ArrayList<>(map.size());
for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
tasks.add(create(entry.getKey(), entry.getValue()));
}
return Tasks.whenAll(tasks);
}
@Override
public V read(K key) {
return get(key);
}
@Override
public Task<V> delete(K key) {
final V oldValue = get(key);
final Continuation<Void, V> onDelete = new Continuation<Void, V>() {
@Override
public V then(@NonNull Task<Void> task) throws Exception {
task.getResult();
return oldValue;
}
};
return mDatabaseReference.child(key.toString())
.setValue(null)
.continueWith(onDelete);
}
ObservableArrayMap
We create an abstract FirebaseArrayMap
extending ObservableArrayMap
and implementing both ChildEventListener
and CRUD
.
public abstract class FirebaseArrayMap<K extends Object, V> extends
ObservableArrayMap<K, V> implements ChildEventListener, CRUD<K, V> {
private final DatabaseReference mDatabaseReference;
public abstract Class<V> getType();
public FirebaseArrayMap(@NonNull DatabaseReference databaseReference) {
mDatabaseReference = databaseReference;
}
The ChildEventListener
will use the super methods, turning the ObservableArrayMap
into a local cache.
Consequently, when a write operation is successfully completed (or a remote change happens), the ChildEventListener
will automatically update our Map
@Override
public void onCancelled(DatabaseError error) {
Log.e(TAG, error.getMessage(), error.toException());
}
@Override
public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
if (snapshot.exists()) {
super.put((K) snapshot.getKey(), snapshot.getValue(getType()));
}
}
@Override
public void onChildChanged(DataSnapshot snapshot, String previousChildName) {
super.put((K)snapshot.getKey(), snapshot.getValue(getType()));
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
super.put((K)snapshot.getKey(), snapshot.getValue(getType()));
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
super.remove(dataSnapshot.getKey());
}
The Contract
The CRUD
interface is required in order to not break Map
contract. For example, put
returns the previous value when a new value is inserted at the given position, but this operation is now asynchronous.
For writing operations, the hack here is using CRUD
for non blocking and Map
for blocking operations:
@Override
@WorkerThread
public V put(K key, V value) {
try {
return Tasks.await(create(key, value));
} catch (ExecutionException e) {
return null;
} catch (InterruptedException e) {
return null;
}
}
Data Binding
For free, now you also have Android Data Binding for your Map
:
@Override
protected onCreate() {
mUserMap = new UserArrayMap();
mChangedCallback = new OnUserMapChanged();
}
@Override
protected void onResume() {
super.onResume();
mUserMap.addOnMapChangedCallback(mChangedCallback);
}
@Override
protected void onPause() {
mUserMap.removeOnMapChangedCallback(mChangedCallback);
super.onPause();
}
and
static class OnUserMapChanged extends OnMapChangedCallback<FirebaseArrayMap<String, User>, String, User> {
@Override
public void onMapChanged(FirebaseArrayMap<String, User> sender, String key) {
Log.e(TAG, key);
Log.e(TAG, sender.get(key).toString());
}
}
Remember to (un)register your callback in onResume
and onPause
in order to avoid memory leaking caused by ChildEventListener
updates.