0

I need the result of a Firestore query for the return statement. The method returns an empty List, as the return statement is ran before the task is completed.

This is the problematic code inside my DatabaseManager class:

public List<Image> getImagesFromDatabase(Query query){

    List<Image> dataList= new ArrayList<Image>();
    query.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
        @Override
        public void onComplete(@NonNull Task<QuerySnapshot> task) {
            if (task.isSuccessful()) {
                for (QueryDocumentSnapshot document : task.getResult()) {
                    Image image=document.toObject(Image.class);
                    image.setDocumentId(document.getId());
                    dataList.add(image);
                }
            } else {
                Log.d("DatabaseManager", "Error getting documents: ", task.getException());
            }
        }
    });
    return dataList;
}

And this is the code which uses the List which previous method returns:

  private int getMaxNumberId(){
    Query query=dbManager.getCollectionReference().orderBy("numberId", Query.Direction.DESCENDING).limit(1);
    List<Image> images=dbManager.getImagesFromDatabase(query);
    Image maxImage=images.get(0);
    return maxImage.getNumberId();
}

I have read about Future which, as I understand, isn't a good solution, and about making a callback interface, but I don't know how to implement that solution due to the return statement.

spaceBar
  • 1
  • 3
  • Can this help you: https://stackoverflow.com/a/56403095/14759470 – SlothCoding Feb 05 '21 at 23:36
  • Please check the duplicate to see how you can solve that. There are two answers there. The second one is using get() and the corresponding callback. – Alex Mamo Feb 06 '21 at 10:23
  • @AlexMamo thanks, that works, but it's a really messy solution, at least for me, because I have a lot of other code that depends on the result of the get(), therefore, all of that code has to be inside onResponse(). – spaceBar Feb 06 '21 at 19:41
  • 1
    Another approach might be to use Kotlin Coroutines. – Alex Mamo Feb 06 '21 at 22:04

2 Answers2

2

The problem you're facing with this code is that it's not running in a synchronous manner. That basically means that it isn't being executed exactly as it's read. What getImagesFromDatabase() is doing is:

  1. Creates a new ArrayList and sets it to your dataList variable.
  2. Sends off your query to firebase (through get()) and forgets it for the time being.
  3. Continues with your code, thus returning your empty dataList ArrayList.

This is an asynchronous implementation as it doesn't wait around for the answer before continuing. So you need a way to let your code know that Firebase has returned your results when onComplete() is executed because right now the only thing it does is set your results to an outdated instance which has already been returned. There are 3 solutions which I can think of in this situation.

  • Await the result

    For the first solution, a useful method should be offered by the Firebase SDK to await your query, in other words freeze the thread in which it was called (most likely the UI thread - so not a great idea). After searching for a while, I see that Firebase SDK offers Tasks.await() but it seems to give varied results and it's not a recommended approach so let's move on to the next one.

Note: According to this SO question Firebase doesn't even offer a synchronous implementation so that also rules out this option.

  • Setup a callback

    1. Create an interface to represent your desired methods (ie onSuccess, onFailure).
    2. Instantiate said interface through an anonymous class to a variable and pass this variable to your getImagesFromDatabase() method.
    3. Call the variable's method (ie variable.onSuccess(results)) in Firebase's onComplete() method
    4. Expect your onSuccess() implementation in the anonymous class to be called, where you can probably use all the resources you need with the result.

    For more details on this check out this SO answer as well as this one

  • Make your variable Observable

    Any Observable variables notify you of when a change has happened so long as you have set an Observer on them. You may do that with Android's .observe(lifecycleOwner, Observer{}) method or by using DataBinding if you have it set up in your project. This is also in terms with Android's guidelines for event-based programming, MVVM architecture and in general will save you a lot of trouble as a mindset.

    You can turn your ArrayList into an ObservableArrayList, instantiate it and return it as you are. Then you wanna set a listener/observer on this variable and finally update its value in onComplete().

Take into account that you may need to refactor some of your methods (such as getMaxNumberId()) to behave in a more reactive manner. Specially whenever you will be communicating with Firebase, know that you will have to work with an event-based manner and not a sequential one with common functional returns (unless they're Observables, of course!).

Hope this helped, Panos.

Panos Gr
  • 667
  • 5
  • 13
0

As Panos Gr, you can use an interface. First you create a new interface, same class also can.

 private interface IQuery{
      void onSuccess(List<Image> imageList);
 }

Add the new parameter to the method getImagesFromDatabase

 public List<Image> getImagesFromDatabase(Query query, IQuery iQuery ){
        List<Image> dataList= new ArrayList<>();
        query.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
            @Override
            public void onComplete(@NonNull Task<QuerySnapshot> task) {
                if (task.isSuccessful()) {
                    for (QueryDocumentSnapshot document : task.getResult()) {
                        Image image=document.toObject(Image.class);
                        image.setDocumentId(document.getId());
                        dataList.add(image);
                        iQuery.onSuccess(dataList);
                    }
                } else {
                    Log.d("DatabaseManager", "Error getting documents: ", task.getException());
                }
            }
        });
        return dataList;
 }

And then call the interface at the method getMaxNumberId

  private int getMaxNumberId(){
        Query query=dbManager.getCollectionReference().orderBy("numberId", Query.Direction.DESCENDING).limit(1);
        dbManager.getImagesFromDatabase(query, new IQuery() {
            @Override
            public void onSuccess(List<Image> imageList) {
                Image maxImage=imageList.get(0);
                return maxImage.getNumberId();
            }
        });
        Image maxImage=images.get(0);
        return maxImage.getNumberId();
  }

I hope you will get the idea

Ticherhaz FreePalestine
  • 2,738
  • 4
  • 20
  • 46