1

I want to store locally the data I am reading from the cloud. To achieve this I am using a global variable(quizzes) to hold all the data.

For this, when I am building my Quiz objects, I need to make sure that before I am creating them, the relevant data has been already downloaded from the cloud. Since when reading data from firestore, it happens asynchronously.

I didn't enforced this (waiting for the read to finish) before -I just used onSuccess listeners, and I encountered synchronization problem because the reading tasks weren't finished before I created my Quiz objects with the data from the cloud.

I fixed this with a very primitive way of "busy waiting" until the read from the cloud is complete. I know this is very stupid, a very bad practice, and making the application to be super slow, and I am sure there is a better way to fix this.

 private void downloadQuizzesFromCloud(){


    String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
    final FirebaseFirestore db = FirebaseFirestore.getInstance();
    CollectionReference quizzesRefrence = db.collection("users").document(user_id).collection("quizzes");

    Task<QuerySnapshot> task = quizzesRefrence.get();
    while(task.isComplete() == false){
        System.out.println("busy wait");
    }


    for (QueryDocumentSnapshot document : task.getResult()) {
        Quiz quizDownloaded = getQuizFromCloud(document.getId());
        quizzes.add(quizDownloaded);
    }

}

I looked online in the documentation of firestore and firebase and didn't find anything that I could use. (tried for example to use the "wait" method) but that didn't help.

What else can I do to solve this synchronization problem?

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
genericname
  • 167
  • 3
  • 11
  • you can use of MutableLiveData, and observe for onChange and do what you want with your data – medyas Dec 29 '18 at 14:09
  • As a general note, no one should be trying to convert an async operation into a sync operation in a mobile app. This will cause problems. Please read about why all the Firebase APIs are async: https://medium.com/google-developers/why-are-firebase-apis-asynchronous-callbacks-promises-tasks-e037a6654a93 – Doug Stevenson Dec 29 '18 at 19:14

2 Answers2

1

I didn't understand if you tried this solution, but I think this is the better and the easier: add an onCompleteListener to the Task object returned from the get() method, the if the task is succesfull, you can do all your stuff, like this:

private void downloadQuizzesFromCloud(){


String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
CollectionReference quizzesRefrence = db.collection("users").document(user_id).collection("quizzes");

quizzesRefrence.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
        @Override
        public void onComplete(@NonNull Task<QuerySnapshot> task) {
            if (task.isSuccesful()) {
               for (QueryDocumentSnapshot document : task.getResult()) {
                    Quiz quizDownloaded = getQuizFromCloud(document.getId());
                    quizzes.add(quizDownloaded);
               }
             }
         });
}
}

In this way, you'll do all you have to do (here the for loop) as soon as the data is downloaded

Luca Murra
  • 1,848
  • 14
  • 24
  • 1
    This is the solution I started with, however, it didn't work. In the implementation you suggest, if I want to use the variable quizzes (for example after the read, or in a different function), then the task might not be finished and the data in quizzes wouldn't be correct – genericname Dec 29 '18 at 14:57
  • 1
    Why do you say that the task hasn't been finished? If we are in onComplete method, the task has been completed – Luca Murra Dec 29 '18 at 15:05
  • 1
    In the scope of the OnComplete, yes, of course, the task is finished, however, if I were to use the variable "quizzes" somewhere else in my code, (for example just before the function "downloadQuizzesFromCloud" is finished) It is not promised that the task has finished and I could have incorrect data in "quizzes" variable. I know this because I tried and debugged what you suggested (that's what caused the synchronization problems in the first place) – genericname Dec 29 '18 at 15:12
  • 3
    Trying to turn an asynchronous API call into a synchronous sequence is a recipe for headaches. If you have more code that needs access to the loaded quizes, that code should *inside* the `onComplete` or invoked from there. There is no other way around it. Also see my answer here for a longer explanation/example: https://stackoverflow.com/questions/50434836/getcontactsfromfirebase-method-return-an-empty-list/50435519#50435519 – Frank van Puffelen Dec 29 '18 at 15:20
  • Ok, now I understood; I can only sugguest you to use a boolean variable (like "downloaded") and use the 'quizzes' object only if that bool is true, but I think you already tried it. I can't help you, an online request can't be synchronous; the only thing you can do is stopping the process like in your question, but doing it in a separate Thread to avoid ANR – Luca Murra Dec 29 '18 at 15:23
1

You can make your own callback. For this, make an interface

public interface FireStoreResults {
    public void onResultGet();
}

now send this call back when you get results

public void readData(final FireStoreResults){
    db.collection("users").document(user_id).collection("quizzes")
    .get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
            @Override
            public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
                for (QueryDocumentSnapshot document : task.getResult()) {
                     Quiz quizDownloaded = getQuizFromCloud(document.getId());
                     quizzes.add(quizDownloaded);
                 }
                results.onResultGet();
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                results.onResultGet();
            }
        });   
}

Now in your activity or fragment

new YourResultGetClass().readData(new FireStoreResults(){
             @Override
             public void onResultGet() {
                    new YourResultGetClass().getQuizzes(); //this is your list of quizzes
                    //do whatever you want with it
            }

Hope this makes sense!

M.AQIB
  • 359
  • 5
  • 11