Multiply reading of document references from Firestore using tasks - android

I have a Map of references which I read from Firestore. these refs lead me to documents that I'm willing to use their data to create an instance of my class 'Contact'.
In order to do that I've created a list of tasks which every task of it uses its ref to read from Firestore and retrieve the needed data.
Once it's all done I use Tasks.whenAll(tasks).addOnSuccessListener() willing to retrive my new array of Contacts.
On this method, 'contacts' is empty and 'data' is full of document references.
I expected Tasks.whenAll(tasks) to being called only when all this reading using the refs has completed, however it's being called immediately, therefore - nothing happens.
private void createContactArray(final ArrayList<Contact> contacts, final Map<String, DocumentReference> data) {
List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
for (final Map.Entry<String, DocumentReference> entry : data.entrySet()) {
tasks.add(db.document(entry.getValue().getPath()).get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document.exists()) {
Map<String,String> contactDetails = (Map<String, String>) document.getData().get(entry.getKey());
Contact contact = createContact(contactDetails);
if(contact != null){ contacts.add(contact);}
} else {
Log.d(ACTION_FETCH_CONTACT_LIST,"There was ref problem with " + entry.getKey());
}
}else {
Log.d(ACTION_FETCH_CONTACT_LIST, "get failed with ", task.getException());
}
}
}));
}
Tasks.whenAll(tasks).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
sendBroadcastActionContactList(contacts);
}
});
I would like Tasks.whenAll to be called once its all finished and not right away. I wish to have a proper explanation for the issue and a decent code that should do the job instead of mine.
I really appreciate your help!

You are using the APIs incorrectly. You should be collecting tasks returned by get() into an array, instead of immediately adding a callback to each one. Pass that list of tasks to Tasks.whenAll(). Then, in the callback for the task returned by Tasks.whenAll, you can examine each DocumentSnapshot results.

Related

Retrieve documents from firestore according to its position in the List

I have two collections in my firestore database, the first is list of all the documents (BlockList), and the second for the users. when the user bookmark post on Recyclerview on the app, send the id of the document and timestamp to sub-collection (Favorites) under Users Collection.
i retrieve the list of documents based on this ids from the main collection (BlockList), but i want to arrange them according to timestamp for each user, so i've tried to order them by timestamp before adding them to Arraylist and Log the list, i've got the correct result. but the recyclerview still retrieved them ascending order by document ID !
firebaseFirestore.collection("Users")
.document(userId).collection("Favorites").orderBy("timestamp", Query.Direction.DESCENDING).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<String> list = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
list.add(document.getId());
}
Log.d(TAG, list.toString());
if (list.size() != 0){
blockReffav = firebaseFirestore.collection("BlockList")
.whereIn(FieldPath.documentId(), list);
blockReffav.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if(task.isSuccessful()){
onFirestoreTaskComplete.blockListDataAdded(task.getResult().toObjects(BlockListModel.class));
} else {
onFirestoreTaskComplete.onError(task.getException());
}
}
});
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Given how you currently retrieve the documents:
blockReffav = firebaseFirestore.collection("BlockList")
.whereIn(FieldPath.documentId(), list);
There is no way to specify the order of the IDs in the list in this query, so you will get them back in (the default) order of document IDs.
I see two options:
Request the documents one by one, so that you get them in the correct order.
Re-order the documents in your application code to match the order in the list.
I'd recommend the latter, as it's likely to be both simpler and more reliable.

Firestore task with for each loop

I'm trying to retrieve document references from Firestore in a for loop (without iterating through the subset). I need to wait for the loop to finish, wait for data to be received and on success, submit this data to the Firestore. Currently, my method does not wait for data to be received what so ever since it is async.
It would probably be a good idea to create a method that returns a Task and then wait for a result. Suggestions?
ArrayList<String> documentPath = new ArrayList<>();
private void getDocumentRef() {
try {
for (String path : documentPath) {
db.document(path).get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful() && task.getResult() != null) {
if (task.getResult().exists()) {
references.add(task.getResult().getReference());
}
}
}
});
}
} catch (Exception e) {
}
}
If I understood your issue correctly, following method should work:
private void getDocumentRef(List<String> documentPaths) {
List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
for (String path : documentPaths) {
tasks.add(db.document(path).get());
}
Task<List<DocumentSnapshot>> finalTask = Tasks.whenAllSuccess(tasks);
finalTask.addOnSuccessListener(new OnSuccessListener<List<DocumentSnapshot>>() {
#Override
public void onSuccess(List<DocumentSnapshot> documentSnapshots) {
/* This list contains all the retrieved document snapshots. Now iterate
through this list to get the document references.*/
for (DocumentSnapshot snapshot : documentSnapshots) {
references.add(snapshot.getReference());
}
/* Here u can do whatever u want with the list named references
because now it has references of all required documents. */
}
});
}
Here, we iterate through the supplied list of paths, create a separate Task for retrieving the document at that path and add this task to a list of Task<DocumentSnapshot>. Then, we supply this list to Tasks.whenAllSuccess() and create a new Task named finalTask. We then attach an OnSuccessListener to finalTask whose onSuccess() method is called when all the supplied tasks get completed. What we get in onSuccess() is a list of DocumentSnapshot of each document. We can now go through this list and get the DocumentReference.
Hope it helps!
For continuing after completing your requests for the selected subset, you would need to keep track of how many queries you're making and how many have completed.
Create a member variable to track the queries and within your OnCompleteListener:
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
mCompleted++; // Update your member variable
if(task.isSuccessful(){
// Do something with your returned data
}else{
// The task failed
}
// Check if the last query has completed
if(mCompleted == numOfQueries){
mCompleted = 0; // Reset the completed queries if you might run this process again
// All of your queries have returned and you can now do something with the complete data set
}
}

firestore onSuccess listener isn't working

I am trying to download some Quiz objects from my database.
The following function is called from onCreate of a certain activity.
private void downloadQuizzesFromCloud(){
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
String user_quizzes_path = "users/".concat(user_id).concat("/quizzes");
Query userQuizzes = db.collection(user_quizzes_path);
userQuizzes.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
quizzes.clear();
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
Toast.makeText(QuizzesActivity.this,"downloaded to list ".concat(String.valueOf(quizzes.size()).concat(" quizzes")), Toast.LENGTH_SHORT).show();
//TODO put in recycle adapter
} else { }
}
});
}
(user_quizzes_path contains the correct path to a collection of Quiz objects stored on the cloud)
I debugged this functions and found out that after the command:
userQuizzes.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>()
The function finishes execution, that is the onComplete cases aren't checked and executed and all this code is just skipped.
I tried to find this on the documentation of firebase but didn't find anything.
Why is this happening and how can I fix this?
Would appreciate some help here, thanks!
The onComplete is called when the read operation has completed from the Firestore servers. If it's not getting called, I can see two possible reasons:
You're not connected to the server. Unless you've read the data before (and it's in the local database that the Firestore client maintains), this means the read never completes locally.
You're not thinking asynchronously. Note that data is read from the server asynchronously, and there may be some time between when you call get() and when onComplete fires. To test if this is the case, put a breakpoint on if (task.isSuccessful()) { and run the app in the debugger. The breakpoint will hit when the data is read from the server.
Use a callback interface. Just like this below.
private void downloadQuizzesFromCloud(Consumer listener) {
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
String user_quizzes_path = "users/".concat(user_id).concat("/quizzes");
Query userQuizzes = db.collection(user_quizzes_path);
userQuizzes.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<Quiz> quizzes = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
listener.onGet(quizzes);
Toast.makeText(QuizzesActivity.this,"downloaded to list ".concat(String.valueOf(quizzes.size()).concat(" quizzes")), Toast.LENGTH_SHORT).show();
//TODO put in recycle adapter
} else { }
}
});
}
interface Consumer {
void onGet(List<Quiz> quizzes);
}

firestore read is asynchronous and I want synchronous behaviour

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?
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
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!

Performance of Chaining Firebase Firestore Query and Document Snapshot Tasks

I'm creating an Android app that uses Firebase Firestore to store some data. Since collection group queries are not yet supported in Firestore, in order to build some of the result sets that I need, I have to do a Query followed by multiple DocumentReference.get() calls to achieve the desired end result.
My implementation is as follows:
PlaylistFacade.java
public static Task<QuerySnapshot> GetPlaylistSubscriptionsByOwner(FirebaseFirestore db, #NonNull String ownerUserId)
{
Query playlistRef = db.collection("playlistSubscriptions")
.whereEqualTo("ownerId", ownerUserId);
return playlistRef.get();
}
public static Task<DocumentSnapshot> GetPlaylist(FirebaseFirestore db, #NonNull String playlistId)
{
return db.collection("playlists").document(playlistId).get();
}
PlaylistActivity.java
PlaylistFacade.GetPlaylistSubscriptionsByOwner(mFirestore, this.GetCurrentUser().getUid())
.continueWithTask(new Continuation<QuerySnapshot, Task<List<Task<?>>>>() {
#Override
public Task<List<Task<?>>> then(#NonNull Task<QuerySnapshot> task) throws Exception {
List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
for(DocumentSnapshot doc:task.getResult().getDocuments())
{
PlaylistSubscription ps = doc.toObject(PlaylistSubscription.class);
ps.setId(doc.getId());
tasks.add(PlaylistFacade.GetPlaylist(mFirestore, ps.getPlaylistId()));
}
return Tasks.whenAllComplete(tasks);
}
})
.addOnCompleteListener(this, new OnCompleteListener<List<Task<?>>>() {
#Override
public void onComplete(#NonNull Task<List<Task<?>>> task) {
if(task.isSuccessful()){
List<Task<?>> tasks = task.getResult();
List<Playlist> playLists = new ArrayList<>();
int errorCount = 0;
for(Task<?> docTask : tasks)
{
if(docTask.isSuccessful())
{
DocumentSnapshot ds = (DocumentSnapshot)docTask.getResult();
Playlist pl = ds.toObject(Playlist.class);
pl.setId(ds.getId());
playLists.add(pl);
}
else
{
Crashlytics.logException(docTask.getException());
errorCount++;
}
}
if(errorCount > 0)
Toast.makeText(PlaylistActivity.this, "Encountered " + errorCount + " errors.", Toast.LENGTH_LONG).show();
else
Toast.makeText(PlaylistActivity.this, "Success", Toast.LENGTH_LONG).show();
}
else
{
Crashlytics.logException(task.getException());
Toast.makeText(PlaylistActivity.this, task.getException().getMessage(), Toast.LENGTH_LONG).show();
}
}
});
The above works just fine, but I'm curious if there may be a better way to do it.
My questions are as follows:
Is chaining the Task objects using List<> the ideal approach? Am I giving up any potential performance by using this approach?
Will using the Task chain still allow me to take advantage of Firebase's ability to pipeline requests?
Will my use of Tasks.whenAllComplete() allow me to conditionally accept failure of some or all results, or does Firebase's pipelining cause errors to propagate across requests such that I should really just use Tasks.whenAllSuccess() and avoid the need check success of each individual request?
Response times of my implementation seem fine on small result sets. Am I likely to get better performance as my result set grows if I build my result set in a Cloud Function before returning it to my app instead?
At what complexity of Firestore actions should I really be using an Executor as demonstrated in the DocSnippets.java sample on the deleteCollection(...) function?
Would using a Transaction to bundle requests ever net me any performance gains? Performance implications of doing reads inside a transaction aren't discussed in the documentation.
Any news of when collection group queries will be available in Firestore? In time for Google IO, perhaps?
Thank you for your time.
Other helpful resources for those who end up here with similar questions:
Doug Stevenson's Firebase Blog Post on Task Wiring

Categories

Resources