1

I am using the Real Time Database from Firebase on java for Android and I am trying to write a set of helper function to facilitate read and write functions from/to the RTDB.

My function looks like the following and is supposed to return a HashMap of my Parking objects on the database; I get a reference to my database and add an onSuccessListener where I iterate through the snapshot and add each Parking object to my HashMap and return the HashMap parkings.

The problem is the function returns parkings with no values in it before the onSuccessListener runs.

public static ArrayList<Parking> getParkingLots() {
    DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
    Task<DataSnapshot> dataSnapshotTask = mDatabase.get();
    ArrayList<Parking> parkings = new ArrayList<Parking>();
    dataSnapshotTask.addOnSuccessListener(new OnSuccessListener<DataSnapshot>() {
        @Override
        public void onSuccess(DataSnapshot dataSnapshot) {
            Iterable<DataSnapshot> parkingsData = dataSnapshot.getChildren();
            for (DataSnapshot parking :
                    parkingsData) {
                parkings.add(parking.getValue(Parking.class));
            }
        }
    });
    return parkings;
}

I tried this implementation as well where I directly try and get the results from the Task datSnapshotTask but I get an exeption thrown java.lang.IllegalStateException: Task is not yet complete.

   public static HashMap<String, Parking> getParkingLots() {
    DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
    Task<DataSnapshot> dataSnapshotTask = mDatabase.get();
    Iterable<DataSnapshot> parkingsData = dataSnapshotTask.getResult().getChildren();
    HashMap<String, Parking> parkings = new HashMap<String, Parking>();
    for (DataSnapshot parking :
            parkingsData) {
        parkings.put(parking.getKey(), parking.getValue(Parking.class));
    }

    return parkings;
}

Is there a way to get the results from the Task in an await fashion ?

Sergio
  • 27,326
  • 8
  • 128
  • 149
Avviator
  • 13
  • 1
  • 4

2 Answers2

2

I don't think it's possible to do in "an await fashion" in Java.

But in Kotlin we can use coroutines for that.

suspend fun getParkingLots(): HashMap<String, Parking> {
    val database: DatabaseReference = FirebaseDatabase.getInstance().reference
    val task: Task<DataSnapshot> = database.get()
    val deferredDataSnapshot: Deferred<DataSnapshot> = task.asDeferred()
    val parkingsData: Iterable<DataSnapshot> = deferredDataSnapshot.await().children // or can use just task.await()
     
    // ... use parkingsData
}

Using Task.asDeferred extension function we can convert a Task into a Deferred object. Or can use Task.await() without converting the task to the Deferred object.

To use Task.asDeferred() extension function use dependency:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.2'

To call suspend function we need to launch a coroutine

someCoroutineScope.launch {
    val parkings = getParkingLots()
}

someCoroutineScope is a CoroutineScope instance. In android it can be viewModelScope in ViewModel class and lifecycleScope in Activity or Fragment, or some custom CoroutineScope instance. Dependencies:

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
Sergio
  • 27,326
  • 8
  • 128
  • 149
  • 1
    This is a very good answer. The official documentation of firestore should have a link to this instead of leading people to create a callback-hell for complex cases. – mcy Jul 03 '22 at 08:33
0

Data is loaded from Firebase asynchronously, since it may have to come from the server. While the data is being loaded, your main code continues to run. Then when the data is available, the task completes and your onSuccess gets called.

It's easiest to see what this means in practice by running in a debugger, or by adding some logging:

DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
Log.i("Firebase", "1. Starting to load data");
Task<DataSnapshot> dataSnapshotTask = mDatabase.get();
for (DataSnapshot parking: parkingsData) {
    Log.i("Firebase", "2. Got data");
}
Log.i("Firebase", "3. After starting to load data");

When you run this code, it prints:

  1. Starting to load data

  2. After starting to load data

  3. Got data

This is probably not the order that you expected, but it actually working as intended. It also explains why you're not getting a result from your getParkingLots function: by the time your return parkings runs, the parkings.add(parking.getValue(Parking.class)) hasn't been called yet!


The solution for this is always the same: any code that needs the data from the database, needs to be inside your onSuccess method or be called from there. This means you can't return the parking lots from the function, but you can pass in a callback that takes them as a parameter. Or you could return a Task<HashMap<String, Parking>> pretty similar to what Firebase does got get().

For an example of the callback, see my longer explanation on: getContactsFromFirebase() method return an empty list

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thank you for that. Could you please tell can we make the next line of code await for execution of the current task, without onSuccessListener? – Liker777 Dec 19 '21 at 10:59
  • There is no way to make asynchronous code behave synchronously. If you check my answer to the question I linked (and more questions I linked from there), you will find various strategies to deal with the asynchronous nature of model cloud API calls. – Frank van Puffelen Dec 19 '21 at 16:53