Wait for Firestore queries to complete - java

I am currently trying to run multiple queries in firestore and want to wait for them to all complete before executing the next code. I've read up on several possible avenues but I have yet to find a good Android example.
public HashMap<String,Boolean> multipleQueries(String collection, String field, final ArrayList<String> criteria) {
HashMap<String,Boolean> resultMap = new HashMap<>();
for (int i = 0; i < criteria.size(); i++){
final int index = i;
db.collection(collection).whereEqualTo(field,criteria.get(i)).limit(1)
.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if(task.isSuccessful()) {
if(task.getResult().size() != 0 ){
resultMap.put(criteria.get(index),true);
} else {
resultMap.put(criteria.get(index),false);
}
} else {
resultMap.put(criteria.get(index),false);
}
}
});
}
return resultMap;
}

Since get() returns a Task, you can use Tasks.whenAll(...).
But you won't be able to return a List from this function, since all data is asynchronously loaded. The best you can do is return the result from Task.whenAll(...), which is itself a Task. See Doug Stevenson's great article on becoming a task master: https://firebase.googleblog.com/2016/10/become-a-firebase-taskmaster-part-4.html

Related

Index:0 Size:0 when fetching item from firebase [duplicate]

This question already has answers here:
getContactsFromFirebase() method return an empty list
(1 answer)
Setting Singleton property value in Firebase Listener
(3 answers)
Closed last year.
I am trying to fetch a value from firebase like so:
private String getVCount(String mPath) {
ArrayList<String> strings = new ArrayList<>();
FirebaseDatabase.getInstance().getReference("cars").child(mPath).child("maserati")
.get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
#Override
public void onComplete(#NonNull #NotNull Task<DataSnapshot> task) {
if(task.isSuccessful()){
strings.add(task.getResult().getValue().toString());
}else{
strings.add("0");
}
}
});
Log.d("mString", String.valueOf(strings.size()));
return strings.get(0);
}
I double checked the path, and even added an else condition just to make it so that the strings array list has at least one value in it.
The value in the database does exist, and does not exist in some cases--hence the else statement to add a default value of 0. I'm not sure if this is the correct way to do that though.
Additionally, I'm still getting the error: Index: 0 Size: 0--meaning that the strings array list is empty.
Any idea why this may be so?
Thanks.
Getting data from Firebase Realtime Database is an async task. So,
Log.d("mString", String.valueOf(strings.size()));
return strings.get(0);
might be getting called before the code inside addOnCompleteListener.
If you want to have your getVCount to have a way to return the result, one way to do this is by creating your own callback like this:
public interface Callback {
void onCallback(String result);
}
private void getVCount(String mPath, Callback callback) {
ArrayList<String> strings = new ArrayList<>();
FirebaseDatabase.getInstance().getReference("cars").child(mPath).child("maserati")
.get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
#Override
public void onComplete(#NonNull #NotNull Task<DataSnapshot> task) {
if(task.isSuccessful()){
strings.add(task.getResult().getValue().toString());
}else{
strings.add("0");
}
Log.d("mString", String.valueOf(strings.size()));
callback.onCallback(strings.get(0));
}
});
}
And call it like this:
getVCount(new Callback() {
#Override
public void onCallback(String result) {
Log.d(TAG, result);
}
});

Array call returns null although I have updated data [duplicate]

Before adding a new data into the firestore, i want to check already a data of the same kind exists in the database or not.if already a data was present means i want to prevent the user from entering duplicate data in the database.
In my case it is like a appointment booking if already a booking for the same time exists,i want to prevent to users to book on the same time.i tried using query function but it is not preventing duplicate data entering.someone plz help me
private boolean alreadyBooked(final String boname, final String bodept, final String botime) {
final int[] flag = {0};
CollectionReference cref=db.collection("bookingdetails");
Query q1=cref.whereEqualTo("time",botime).whereEqualTo("dept",bodept);
q1.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
for (DocumentSnapshot ds : queryDocumentSnapshots) {
String rname, rdept, rtime;
rname = ds.getString("name");
rdept = ds.getString("dept");
rtime = ds.getString("time");
if (rdept.equals(botime)) {
if (rtime.equals(botime)) {
flag[0] = 1;
return;
}
}
}
}
});
if(flag[0]==1){
return true;
}
else {
return false;
}
}
Loading data from Cloud Firestore happens asynchronously. By the time you return from alreadyBooked, the data hasn't loaded yet, onSuccess hasn't run yet, and flag still has its default value.
The easiest way to see this is with a few log statements:
private boolean alreadyBooked(final String boname, final String bodept, final String botime) {
CollectionReference cref=db.collection("bookingdetails");
Query q1=cref.whereEqualTo("time",botime).whereEqualTo("dept",bodept);
System.out.println("Starting listener");
q1.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
System.out.println("Got data from Firestore");
}
});
System.out.println("Returning");
}
If you run this code it will print:
Starting listener
Returning
Got data from Firestore
That's probably not the order you expected. But it perfectly explains why you always get false when calling alreadyBooked: the data simply didn't come back from Firestore in time.
The solution for this is to change the way you think about the problem. Your current code has logic: "First check if it is already booked, then add a new item". We need to reframe this as: "Start checking if it is already booked. Once we know that is isn't, add a new item." In code this means that all code that needs data from Firestore must be inside the onSuccess or must be called from there.
The simplest version is to move the code into onSuccess:
private void alreadyBooked(final String boname, final String bodept, final String botime) {
CollectionReference cref=db.collection("bookingdetails");
Query q1=cref.whereEqualTo("time",botime).whereEqualTo("dept",bodept);
q1.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
boolean isExisting = false
for (DocumentSnapshot ds : queryDocumentSnapshots) {
String rname, rdept, rtime;
rname = ds.getString("name");
rdept = ds.getString("dept");
rtime = ds.getString("time");
if (rdept.equals(botime)) {
if (rtime.equals(botime)) {
isExisting = true;
}
}
}
if (!isExisting) {
// TODO: add item to Firestore
}
}
});
}
While this is simple, it makes alreadyBooked less reusable since now it contains the code to insert the new item too. You can solve this by defining your own callback interface:
public interface AlreadyBookedCallback {
void onCallback(boolean isAlreadyBooked);
}
private void alreadyBooked(final String boname, final String bodept, final String botime, AlreadyBookedCallback callback) {
CollectionReference cref=db.collection("bookingdetails");
Query q1=cref.whereEqualTo("time",botime).whereEqualTo("dept",bodept);
q1.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
for (DocumentSnapshot ds : queryDocumentSnapshots) {
String rname, rdept, rtime;
rname = ds.getString("name");
rdept = ds.getString("dept");
rtime = ds.getString("time");
if (rdept.equals(botime)) {
if (rtime.equals(botime)) {
isExisting = true;
}
}
}
callback.onCallback(isExisting)
}
});
}
And then you call it as:
alreadyBooked(boname, bodept, botime, new AlreadyBookedCallback() {
#Override
public void onCallback(boolean isAlreadyBooked) {
// TODO: insert item
}
});
Also see (many of these are for the Firebase Realtime Database, where the same logic applies):
getContactsFromFirebase() method return an empty list
Doug's blog post on asynchronous callbacks
Setting Singleton property value in Firebase Listener
Android + Firebase: synchronous for into an asynchronous function
Is it possible to synchronously load data from Firebase?
Querying data from firebase

Problems with array in addChildEventListener (Firebase)

Integer scores[] = new Integer[10];
int count = 0;
I have some data that I want to put into array. I put it in onChildAdded, but if I need to get the data from the array out of the onChildAdded, console shows "null".
If I will try to get the data of array into onChildAdded, it will be
success
data.child("scores").addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
count++;
scores[count] = dataSnapshot.getValue(Integer.class);
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
int i = scores[1];
IMPORTANT MOMENT
For example, If I will use operation FOR
for (int i = 0; i < 10; ++i) {
scores[i] = i;
}
int i = scores[3];
i will not be null
Firebase APIs are asynchronous, meaning that each one of those methods: onChildAdded(), onChildChanged() etc, return immediately after it's invoked, and the callback from the Task it returns, will be called some time later. There are no guarantees about how long it will take. So it may take from a few hundred milliseconds to a few seconds before that data is available. Because that method returns immediately, the scores array has not have been populated from the callback yet and that's why is empty.
Basically, you're trying to use a value synchronously from an API that's asynchronous. That's not a good idea. You should handle the APIs asynchronously as intended.
A quick solve for this problem would be to move the following lines of code:
for (int i = 0; i < 10; ++i) {
scores[i] = i;
}
int i = scores[3];
Inside the onChildAdded() method, where the data is available.
If you need to loop your score array outside the callback, I recommend you see the last part of my anwser from this post in which I have explained how it can be done using a custom callback. You can also take a look at this video for a better understanding.

Firestore notice everything has loaded

My Firestore Database structure looks like this:
...a Collection with Routine Objects.
...a Collection with Workout Objects. With the attributes
-> RoutineKey: Stores the Key of the Routine which the Workout is from
-> ExerciseEntryKeys: ArrayList<String> of the Keys of the ExerciseEntry from the Workout
...a Collection with ExerciseEntries Objects.
Now I want to load every Workout from a Routine and the ExerciseEntries of a Workout. To do this, I do the following after I have loaded a Routine Object.
for (final DocumentSnapshot doc : documentSnapshots.getDocuments()) {
final WorkoutSNR workout = doc.toObject(WorkoutSNR.class);
workout.setKey(doc.getId());
workoutsFromRoutine.add(workout);
fm.getColRefExerciseEntries().whereEqualTo("workoutKey", workout.getKey()).get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
if (documentSnapshots.isEmpty()) {
prg.setVisibility(View.GONE);
processData();
} else {
for (int i = 0; i < workout.getExcersiseEntryKeys().size(); i++) {
fm.getDocRefExerciseEntrie(workout.getExcersiseEntryKeys().get(i)).get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
final ExcersiseEntrySNR entry = documentSnapshot.toObject(ExcersiseEntrySNR.class);
entry.setKey(documentSnapshot.getId());
workout.getExcersises().add(entry);
processData();
prg.setVisibility(View.GONE);
Collections.sort(workout.getExcersises(), new Comparator<ExcersiseEntrySNR>() {
#Override
public int compare(ExcersiseEntrySNR e1, ExcersiseEntrySNR e2) {
if (e1.getPosition() < e2.getPosition()) {
return -1;
} else if (e1.getPosition() > e2.getPosition()) {
return 1;
} else {
return 0;
}
}
});
}
});
}
}
}
});
}
}
});
This works like it should but as you can see I call:
processData();
prg.setVisibility(View.GONE);
Collections.sort(workout.getExcersises(), new Comparator<ExcersiseEntrySNR>() {
#Override
public int compare(ExcersiseEntrySNR e1, ExcersiseEntrySNR e2) {
if (e1.getPosition() < e2.getPosition()) {
return -1;
} else if (e1.getPosition() > e2.getPosition()) {
return 1;
} else {
return 0;
}
}
});
Evertime an ExerciseEntry has been successfully loaded. This is very unnecessary and I want to call this code only once everything(Every ExerciseEnry for every Workout of an Routine).
What is the best way to notice everything has been loaded? Does Firestore provide any function for this?
I have tried having an Integer that counts the number of successful ExerciseLoads and Workout loads but I can only access final variables inside a nested class(Is that how its called?).
How do I know when the data is completely loaded from the database?
You can add a flag to each Routine and Workout objects with the value of false and once you have downloaded those objects, to set the value to true but this is not how things are working with Firestore. You cannot know when an object from the database is completed downloaded becase Cloud Firestore is also a realtime database and getting data might never complete. That's why is named a realtime database because in any momemnt the data under those Routine and Workout objects can be changed, properties can be added or deleted.
You can use a CompletionListener only when you write or update data and you'll be notified when the operation has been acknowledged by the Database servers but you cannot use this interface when reading data.
So if anyone is wondering what my Solution at the end is, here is my current Code:
fm.getColRefWorkoutSNR().whereEqualTo("routineKey", routineKey).get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
final int workoutSize = documentSnapshots.getDocuments().size();
for (final DocumentSnapshot doc : documentSnapshots.getDocuments()) {
final WorkoutSNR workout = doc.toObject(WorkoutSNR.class);
workout.setKey(doc.getId());
workoutsFromRoutine.add(workout);
fm.getColRefExerciseEntries().whereEqualTo("workoutKey", workout.getKey()).get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
if (documentSnapshots.isEmpty()) {
prg.setVisibility(View.GONE);
processData();
} else {
if (workout.getExcersiseEntryKeys().size() > 0) {
for (int i = 0; i < workout.getExcersiseEntryKeys().size(); i++) {
fm.getDocRefExerciseEntrie(workout.getExcersiseEntryKeys().get(i)).get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
final ExcersiseEntrySNR entry = documentSnapshot.toObject(ExcersiseEntrySNR.class);
entry.setKey(documentSnapshot.getId());
workout.getExcersises().add(entry);
if (workout.getExcersises().size() == workout.getExcersiseEntryKeys().size()) {
increaseFullyLoadedWorkouts();
}
if (fullyLoadedWorkouts == workoutSize) {
processData();
prg.setVisibility(View.GONE);
}
}
});
}
} else {
increaseFullyLoadedWorkouts();
if (fullyLoadedWorkouts == workoutSize) {
processData();
prg.setVisibility(View.GONE);
}
}
}
}
});
}
}
});
As you can see, I check if I have loaded every exercise for an workout and if thats the case I increase a "fullyLoadedWorkout" counter. Then I check if the counter equals the workout size and if thats the case I know that I have "fully" loaded my data.
I know thats not a good way to do this but its the only solution I can imagine at the moment. This seems to be way easier in the Realtime Database and I'm still consider switching back to it. Any suggestions for a better way are welcomed.

Emitting 2 observables, processing the result and then emitting another one

I have the following task to perform:
I need to emit 2 observables (obs1 & obs2) process their results and then call another observable (obs3) and process its results and if possible that while processing the results of obs3 have access to the results of obs1 and obs2.
This is my draft code which doesn't do the trick, how can I alter it.
public void executeFind(String session_id, long template_id, GameModelType game_model) {
Observable<RxMessage<byte[]>> userObs = context.getUser(session_id);
Observable<Game> gameObs = context.findGame(template_id, game_model, GameStateType.WAITING);
Observable.zip(userObs, gameObs, new Func2<RxMessage<byte[]>, Game, GameObject>() {
#Override
public GameObject call(RxMessage<byte[]> userRawReply, ActiveGame game) {
..
..
return context.updateGame(game.getGameData())
.subscribe(new Action1<GameObject>() {
#Override
public void call(GameObject updateReply) {
..
..
}
});
return userReply;
}
});
}
This doesn't really work - I can write a code which uses explicit calls to .flatMap\subscribe for each Observable but results in many nested calls which is obviously poor usage of the framework.
What is the right way to solve this??
Thank you!
EDIT:
I've found this solution to work, but I'm still wondering whether there is a "cleaner" way to achieve this:
public void executeFind(ReplyMessage<JsonObject> replyObj, String session_id, long template_id, GameModelType game_model) throws CommandException {
rx.Observable<GameObject> userObs = context.getUser(session_id);
rx.Observable<Game> gameObs = context.findGame(template_id, game_model, GameStateType.WAITING);
rx.Observable.zip(userObs, gameObs, new Func2<GameObject, Game, List<Object>>() {
#Override
public List<Object> call(GameObject userReply, Game game) {
User user = ...;
final List<Object> results = new ArrayList<Object>(3);
results.add(ErrorCodes.STATUS_OK);
results.add(user);
results.add(game);
context.updateGame(game.getGameData()).subscribe(new Action1<GameObject>() {
#Override
public void call(GameObject updateReply) {
...
}
});
return results;
}
}).subscribe(new Action1<List<Object>>() {
#Override
public void call(List<Object> results) {
int status = (int) results.get(0);
User user = (User) results.get(1);
Game game = (Game) results.get(2);
}
});
}
I would code this thing with the following idea in mind. May be map can be replace with flatMap if that's relevant for your use case. Also note I have only used Java 8 lambdas syntax, but for more readability I strongly advises you to have simple and well named methods (and use them with a method reference) for each of these functions/actions as it will raise understandability of the code (That's what we do on mockito, but everyone should do it in their own code base).
public void executeFind(ReplyMessage<JsonObject> reply_obj, String session_id, long template_id, GameModelType game_model) throws CommandException {
Observable<GameObject> userObs = context.getUser(session_id);
Observable<Game> gameObs = context.findGame(template_id, game_model, GameStateType.WAITING);
Observable.zip(userObs, gameObs, (userReply, game) -> {
User user = ...;
return GameOfUser.gameFound(game, user);
}).map(gou -> {
context.updateGame(gou.gameData()).susbcribe(...);
return gou;
}).subscribe(gou -> ...);
}

Categories