1

TL; DR

Would it be possible to hide all Firebase read and write operations behind a ObservableMap as a Facade Pattern?

So all we would have to do would be:

User oldUser = map.put(user);
User newUser = map.get(primaryKey);

Full Question

According to Firebase documentation, in order to write a value, I would have to define a resource path through aDatabaseReference and set a value.

For example, if we had a User plain-text object, we would set it like:

mDatabase.child("users")
    .push()
    .setValue(user);

For reading the whole users tree, we would have to implement a ChildEventListener. As soon as the new user be part of the tree, it would be received through onChildAdded:

@Override
public void onChildChanged (DataSnapshot snapshot, String previousChildName) {
    Log.i(TAG, snapshot.getValue(User.class));
}

And finally for reading a specific user, we use a ValueEventListener:

mDatabase.child("users")
    .child(primaryKey)
    .setValue(user)
    .addValueEventListener(new ValueEventListener() {
        @Override
            public void onDataChange(DataSnapshot snapshot) {
            Log.i(TAG, snapshot.getValue(User.class));
        }

        @Override
        public void onCancelled(DatabaseError error) {
            Log.w(TAG, "loadPost:onCancelled", error.getMessage());
        }
    });

So would be possible using an ObservableMap as a Facade Pattern, hidding all Firebase read and write operations?

JP Ventura
  • 5,564
  • 6
  • 52
  • 69

1 Answers1

1

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.

JP Ventura
  • 5,564
  • 6
  • 52
  • 69