Is it possible to synchronously load data from Firebase?

I'm trying to update parts of a WebView in my Android app with data I'm getting from a peer connected via Firebase. For that, it could be helpful to execute blocking operations that will return the needed data. For example, an implementation of the Chat example that will wait until another chat participant writes something before the push.setValue() to return. Is such a behavior possible with Firebase?


Solution 1:

import com.google.android.gms.tasks.Tasks;

Tasks.await(taskFromFirebase);

Solution 2:

On a regular JVM, you'd do this with regular Java synchronization primitives.

For example:

// create a java.util.concurrent.Semaphore with 0 initial permits
final Semaphore semaphore = new Semaphore(0);

// attach a value listener to a Firebase reference
ref.addValueEventListener(new ValueEventListener() {
    // onDataChange will execute when the current value loaded and whenever it changes
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        // TODO: do whatever you need to do with the dataSnapshot

        // tell the caller that we're done
        semaphore.release();
    }

    @Override
    public void onCancelled(FirebaseError firebaseError) {

    }
});

// wait until the onDataChange callback has released the semaphore
semaphore.acquire();

// send our response message
ref.push().setValue("Oh really? Here is what I think of that");

But this won't work on Android. And that's a Good Thing, because it is a bad idea to use this type of blocking approach in anything that affects the user interface. The only reason I had this code lying around is because I needed in a unit test.

In real user-facing code, you should go for an event driven approach. So instead of "wait for the data to come and and then send my message", I would "when the data comes in, send my message":

// attach a value listener to a Firebase reference
ref.addValueEventListener(new ValueEventListener() {
    // onDataChange will execute when the current value loaded and whenever it changes
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        // TODO: do whatever you need to do with the dataSnapshot

        // send our response message
        ref.push().setValue("Oh really? Here is what I think of that!");
    }

    @Override
    public void onCancelled(FirebaseError firebaseError) {
        throw firebaseError.toException();
    }
});

The net result is exactly the same, but this code doesn't required synchronization and doesn't block on Android.

Solution 3:

I came up with another way of fetching data synchronously. Prerequisite is to be not on the UI Thread.

final TaskCompletionSource<List<Objects>> tcs = new TaskCompletionSource<>();

firebaseDatabase.getReference().child("objects").addListenerForSingleValueEvent(new ValueEventListener() {

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                Mapper<DataSnapshot, List<Object>> mapper = new SnapshotToObjects();
                tcs.setResult(mapper.map(dataSnapshot));
            }

            @Override
            public void onCancelled(DatabaseError databaseError) { 
                tcs.setException(databaseError.toException());
            }

        });

Task<List<Object>> t = tcs.getTask();

try {
    Tasks.await(t);
} catch (ExecutionException | InterruptedException e) {
    t = Tasks.forException(e);
}

if(t.isSuccessful()) {
    List<Object> result = t.getResult();
}

I tested my solution and it is working fine, but please prove me wrong!

Solution 4:

Here's a longer example based on Alex's compact answer:

import com.google.android.gms.tasks.Tasks;
import com.google.firebase.firestore.CollectionReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QuerySnapshot;
final FirebaseFirestore firestore = FirebaseFirestore.getInstance(); final CollectionReference chatMessageReference = firestore.collection("chatmessages"); final Query johnMessagesQuery = chatMessageReference.whereEqualTo("name", "john");
final QuerySnapshot querySnapshot = Tasks.await(johnMessagesQuery.get());
final List<DocumentSnapshot> johnMessagesDocs = querySnapshot.getDocuments(); final ChatMessage firstChatMessage = johnMessagesDocs.get(0).toObject(ChatMessage.class);

Note that this is not good practice as it blocks the UI thread, one should use a callback instead in general. But in this particular case this helps.