Android how to trigger a process when two async processes finish executing - java

I am new to Android’s background tasks. I am using Firestore to perform the following tasks:
Read a document.
https://firebase.google.com/docs/firestore/query-data/get-data
DBInstance.collection("restaurants")
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : Objects.requireNonNull(task.getResult())) {
// some other code
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
});
Listen to realtime updates of another document. https://firebase.google.com/docs/firestore/query-data/listen
final DocumentReference docRef = DBInstance.collection("users").document(FirebaseAuth.getInstance().getCurrentUser().getUid());
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
String source = snapshot != null && snapshot.getMetadata().hasPendingWrites()
? "Local" : "Server";
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, source + " data: " + snapshot.getData());
// some other code is run
} else {
Log.i(TAG,"no snapshot found");
}
}
});
Since these are asynchronous processes, they are performed at the same time (roughly).
I want to trigger an independent method when 1. is completed AND when 2. return a non-null snapshot. Therefore, when some other code comments above have been completed.
So, I essentially want some background process that sits idle/ listens for the above two conditions and perform a task/call a method that updates certain UI features.
I have briefly read about BroadcastReciever. Is this relevant? or maybe can I create a custom listener that runs in a background thread? Any suggestions would be helpful since I am not sure what to search for in order to find what I want.
solutions that seems to work (partly suggested by Nehal)
This is the same code as above with the blanks filled in
DBInstance.collection("restaurants")
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : Objects.requireNonNull(task.getResult())) {
restaurantsLoaded = true;
updateUI();
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
restaurantsLoaded = false;
}
});
final DocumentReference docRef = DBInstance.collection("users").document(FirebaseAuth.getInstance().getCurrentUser().getUid());
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
String source = snapshot != null && snapshot.getMetadata().hasPendingWrites()
? "Local" : "Server";
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, source + " data: " + snapshot.getData());
usersSnapshotTriggered = true;
udpateUI();
} else {
Log.i(TAG,"no snapshot found");
}
}
});
public void updateUI(){
if(usersSnapshotTriggered && restaurantsLoaded){
// perform the updates
}
}

You can try below solution:
Declare a global int variable, increment that variable in both firebase listener and call someMethod() from both listener.
private int count=0;
DBInstance.collection("restaurants")
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : Objects.requireNonNull(task.getResult())) {
// some other code
}
count++;
someMethod();
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
});
final DocumentReference docRef = DBInstance.collection("users").document(FirebaseAuth.getInstance().getCurrentUser().getUid());
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
String source = snapshot != null && snapshot.getMetadata().hasPendingWrites()
? "Local" : "Server";
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, source + " data: " + snapshot.getData());
// some other code is run
count++;
someMethod();
//Note : this method will call as many times as there is change in this data , so you have to handle according to your requirement
} else {
Log.i(TAG,"no snapshot found");
}
}
});
private void someMethod(){
if(count>=2){
//execute your code
}
}
Hope this will help!!

I want to trigger an independent method when both 1. and 2. have been completed
In your first example, by adding a complete listener, you'll always be able to know when the operation is complete. If the task.isSuccessful() returns true you know for sure that the operation is completed. Besides that, you can also call getResult() to get the elements that are apart of your restaurants collection. Furthermore, the following line of code:
DBInstance.collection("restaurants")
.get()
Returns a Task<QuerySnapshot> object. If you have had two different queries, you could pass both Task objects to Tasks's whenAllSuccess() method, as explained in my answer from the following post:
Firestore - Merging two queries locally
In this way, you'll be able to know when both operations are completed. However, when using the second solution, you cannot know when getting the data from the database is completed because Cloud Firestore is a real-time database and getting data might never complete. That's why is named a real-time database because at any moment the database can be changed, items can be added or deleted.
The only way to partially know if you have all the data in a particular collection is to perform a single value type query on it. Even then, the data may change after that listener is invoked, so all you really have is a snapshot at a particular moment in time.
As a conclusion, the only solution that you have is to use whenAllSuccess() and pass two or even more Task objects.
I have briefly read about BroadcastReciever. Is this relevant?
No, it's not. According to the docs, the BroadcastReceiver class is:
Base class for code that receives and handles broadcast intents sent by Context.sendBroadcast(Intent).
So, it's not the case.
or maybe can I create a custom listener that runs in a background thread?
The Cloud Firestore client already runs all network operations in a background thread. This means that all operations take place without blocking your main thread. Putting it in a background thread does not give any additional benefits.

Related

Android Firebase Firestore query returns empty results from function, but the same code works when included directly

I'm having a trouble with firebase queries in Studio.
I'm trying to abstract a simple query to get a list of object stored in a collection (in my case "users")
I want to create a function stored in a Class that can be called by every fragment into the project.
But I don' t find any method to do that, is to repeate the same instruction the only way to do that?
Here is an example
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
I write these lines of code every time I need them, but I want to create a method that return a List as in this example:
public static List<User> getUsers(FirebaseFirestore db) {
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
} else {
//error
}
return usersList;
}
Data is loaded from Firestore (and most modern cloud APIs) asynchronously, because it may take some time. Instead of blocking the app during that time, the main code continues to execute. Then when the data is available, your addOnCompleteListener callback is executed with that data.
The easiest way to see this is by adding some well-placed logging to your code:
public static List<User> getUsers(FirebaseFirestore db) {
Log.i(LOGIN, "Starting getUsers");
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
Log.i(LOGIN, "Got data");
})
Log.i(LOGIN, "Returning from getUsers");
}
When you run this code, you get the following output:
Starting getUsers
Returning from getUsers
Got data
This is probably not the order you expected, but it completely explains why the code that calls getUsers never sees the data: by the time your return usersList runs, the data hasn't loaded yet and usersList.add(document.toObject(User.class)) has never been called.
The solution is always the same: any code that needs the data from the database, must either be inside the completion callback, be called from there, or be synchronized by some other means.
A simple example is to create a custom callback function:
public interface GetUsersCallback {
void onCallback(List<User> users);
}
You then pass that to getUsers, which can then call it once it's gotten and processed the results from the database:
public static void getUsers(FirebaseFirestore db, GetUsersCallback callback) {
// 👆
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
callback(usersList); // 👈
}
}
And you can then use it like this:
getUsers(new GetUsersCallback() {
#Override
public void onCallback(List<User> users) {
Log.i(LOGIN, "Found "+users.size()+" users");
}
});
Asynchronous loading of data is incredibly common when dealing with cloud APIs, but it's also quite confusing when you first encounter it. I recommend reading some of these answers to learn more about it:
How to check a certain data already exists in firestore or not
How to return a DocumentSnapShot as a result of a method?
Why does my function that calls an API return an empty or null value?
Firebase Firestore get data from collection

Why does the command transaction.update is executed before the carrelloAttuale.prodotti.add() command

I'm trying to get a product from a document form the cloud firestore and then put that product in the shopping cart. When i read (successfully) the product, i try to put it in an arraylist that is declared outside but it doesnt work unless i put final to the variable.
Doing so, when I run the code below, I successfully retrieve the data, but the operation carrelloAttuale.prodotti.add(prod) is executed after the command transaction.update(), so the update doesn't upload nothing different from the start.
//prendo l'utente
FirebaseAuth auth= FirebaseAuth.getInstance();
//mi salvo il codice del prodotto scannerizzato
final String codiceProdottoScannerizzato=String.valueOf(intentData);
final FirebaseFirestore db = FirebaseFirestore.getInstance();
final DocumentReference docRef = db.collection("carrelli").document(auth.getUid());
final DocumentReference docrefprodotti = db.collection("prodotti").document(codiceProdottoScannerizzato);
db.runTransaction(new Transaction.Function<Void>() {
#Override
public Void apply(Transaction transaction) throws FirebaseFirestoreException {
DocumentSnapshot snapshot = transaction.get(docRef);
final Carrello carrelloAttuale = snapshot.toObject(Carrello.class);
docrefprodotti.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document.exists()) {
Prodotti prod=document.toObject(Prodotti.class);
prod.id=codiceProdottoScannerizzato;
prod.totalePezziCarrello=1;
carrelloAttuale.prodotti.add(prod);
Log.d(TAG, "PRODOTTO: " + prod.toString());
} else {
Log.d(TAG, "No such document");
}
} else {
Log.d(TAG, "get failed with ", task.getException());
}
}
});
Log.d(TAG, "CARRELLO FB: " + carrelloAttuale.size());
transaction.update(docRef, "prodotti", carrelloAttuale.getProdotti());
// Success
return null;
}
}).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "Transaction success!");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(TAG, "Transaction failure.", e);
}
});
I expect that the command update is executed after the carrelloAttuale.prodotti.add(prod)
in the debug log the order of tags are:
CARRELLO FB: 0
PRODOTTO: Nome: latte
Data is loaded from Firestore asynchronously, since it may have to be retrieved from the server. To prevent blocking the app, the main code continues while the data is being retrieved. Then when the data is available, your onComplete gets called.
This means that any code that needs the data from the data, must be inside the onComplete method, or be called from there. So something like:
docrefprodotti.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document.exists()) {
Prodotti prod=document.toObject(Prodotti.class);
prod.id=codiceProdottoScannerizzato;
prod.totalePezziCarrello=1;
carrelloAttuale.prodotti.add(prod);
Log.d(TAG, "PRODOTTO: " + prod.toString());
} else {
Log.d(TAG, "No such document");
}
} else {
Log.d(TAG, "get failed with ", task.getException());
}
Log.d(TAG, "CARRELLO FB: " + carrelloAttuale.size());
transaction.update(docRef, "prodotti", carrelloAttuale.getProdotti());
}
});
Also see:
How to return a DocumentSnapShot as a result of a method?
Firebase Firestore get data from collection
"the command update" is executed before "carrelloAttuale.prodotti.add(prod)" is called because the onComplete() method has an asynchronous behaviour and returns immediately. This means that listener will not get invoked until some time later, after the database update operation is complete. There is no guarantee how long it will take. Depending on your connection speed and the state, it may take from a few hundred milliseconds to a few seconds for the update operation to complete.
If you want to use some logic with that data, you must wait until the asynchronous Firebase database operation is complete. This means that you can only use the prod object inside the listener callback itself.
For more informarions, 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.

Accessing data within an inner function in Java with FireBase/FireStore

I am coding an android app using Google's FireStore backend. The code to grab all the documents in a firestore collection is as follows, from the official documentation:
db.collection("cities")
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
Log.d(TAG, document.getId() + " => " + document.getData());
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Where the above code outputs to Log.d(...), I would like to have my program add the results of the document.getData() call to an ArrayList accessible outside the inner class/method. I'm not sure what is the best way to do this. Attempting to change the return the return type of the onComplete method yields errors. Is there a standard way of accessing elements in methods like this?
Declaring a variable and trying to mutate within the class also isn't possible, unless the variable is final, which defeats the point.
That is an asynchronous call (it launches a background process to do the Firebase query, and once that is done it executes your onComplete listener), so you can't expect to have the data in hand immediately after making the database call. For example, if your function looks like
void getData() {
final List<MyData> list = new ArrayList<>();
db.collection("cities")
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
Log.d(TAG, document.getId() + " => " + document.getData());
list.add(new MyData(document.getData()));
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Log.d(TAG, "List size = " + list.size()); // will print 0
// list will be empty here, the firebase call will take hundreds
// to thousands of milliseconds to complete
}
You need to structure your program so that it can wait for the data to arrive. There are a couple ways you could do this. One would be to have list be a class member that gets filled by the onComplete listener (then you have to structure the program to handle data coming in at random times).
Another way would be to have a data handler routine that takes the ArrayList and does something with it. This method could be called from the onComplete listener once you've gotten all the data. For example:
void getData() {
db.collection("cities")
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<MyData> list = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
Log.d(TAG, document.getId() + " => " + document.getData());
list.add(new MyData(document.getData()));
}
processData(list);
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
}
void processData(List<MyData> data) {
// do stuff with the data, or make a copy, or otherwise share it
// with the rest of your program
}
I would like to have my program add the results of the document.getData() call to an ArrayList accessible outside the inner class/method.
In this case, if you are using a model class, you should use toObject() method and add objects of type YourModelClass in an ArrayList like this:
if (task.isSuccessful()) {
List<YourModelClass> list = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
YourModelClass yourModelClass = document.toObject(YourModelClass.class);
list.add(yourModelClass);
//Do what you need to do with your list
}
}
As you can see, I advised you to use the list of YourModelClass objects inside the callback. This is because onComplete() method has an asynchronous behavior and you cannot simply use that list outside the callback because it will always be empty.
Attempting to change the return the return type of the onComplete method yields errors.
Changing the return type of the method will not give you errors but the result will always be an empty list. You cannot return something now that hasn't been loaded yet. A quick solve for this problem would be to use that list of objects only inside the onComplete() method, as I already wrote above, or if you want to use it outside, 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.

Read Data from Cloud FireStore Android

Is this script wrong, because the data I receive is null while I've added data on the Cloud Firestore. I do not use RecyclerView because I only need one data only.
This is the script:
private void getCustomer(){
firestoreDB.collection("customer")
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
customers = new ArrayList<>();
for (DocumentSnapshot doc : task.getResult()) {
Customer customer = doc.toObject(Customer.class);
customer.setId_customer(doc.getId());
customers.add(customer);
}
} else {
// Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
firestoreListener = firestoreDB.collection("customer")
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
if (e != null) {
// Log.e(TAG, "Listen failed!", e);
return;
}
customers = new ArrayList<>();
for (DocumentSnapshot doc : documentSnapshots) {
Customer customer = doc.toObject(Customer.class);
customer.setId_customer(doc.getId());
customers.add(customer);
}
}
});
id_customer = customers.get(0).getId_customer();
}
and this is my firestore:
You cannot use something now that hasn't been loaded yet. With other words, you cannot simply use the following line of code:
id_customer = customers.get(0).getId_customer();
Outside the onSuccess() method because it will always be null due the asynchronous behaviour of this method. This means that by the time you are trying to use the id_customer variable outside that method, the data hasn't finished loading yet from the database and that's why is not accessible.
A quick solve for this problem would be to use that result only inside the onSuccess() method, or if you want to use it outside, I recommend you see the last part of my anwser from this post in which I have exaplined how it can be done using a custom callback. You can also take a look at this video for a better understanding.

Firebase Firestore retrieving data on app start instead of saving it

I got an issue on my Android app that makes it get information from the Firestore Database every time the application is loaded. In the Firebase Docs, they say that persistence is enabled by default, but that seems not to work for me. Tried to add the settings but nothing changed.
FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(true)
.build();
db.setFirestoreSettings(settings);
The query inside onCreate:
db.collection("administradores").whereEqualTo("email", user.getEmail()).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (DocumentSnapshot document : task.getResult()) {
tipo = (long) document.getData().get("tipo");
if (tipo == HomeFragment.EVENTO_CODE){
menu.findItem(R.id.nav_nuevo).setTitle("Nuevo evento");
menu.findItem(R.id.nav_editar).setTitle("Tus eventos");
fragment.tipoEmpresa.setText("Administrador de eventos");
fragment.tusEvLoc.setText("Tus eventos");
}else if(tipo == HomeFragment.COMERCIO_CODE){
menu.findItem(R.id.nav_nuevo).setTitle("Nuevo local");
menu.findItem(R.id.nav_editar).setTitle("Tus tiendas");
}else if(tipo == HomeFragment.BAR_CODE){
menu.findItem(R.id.nav_nuevo).setTitle("Nuevo local");
menu.findItem(R.id.nav_editar).setTitle("Tus establecimientos");
}
}
}
}
});
I am expecting the data to be saved (downloaded) only once, and be updated when there are changes in the database. May this work with the Firestore tools or is it necessary to create an internal database (i.e. SQLite) to save all the data?
use this to get data from cache. if you want to manually save the data you would need to store it in a local database and work out a way to keep the data synced in your local database. the addSnapshotListener runs when any document is added modified or removed in the collection.
db.collection("cities").whereEqualTo("state", "CA")
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot querySnapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen error", e);
return;
}
for (DocumentChange change : querySnapshot.getDocumentChanges()) {
if (change.getType() == Type.ADDED) {
Log.d(TAG, "New city:" + change.getDocument().getData());
}
String source = querySnapshot.getMetadata().isFromCache() ?
"local cache" : "server";
Log.d(TAG, "Data fetched from " + source);
}
}
});

Categories

Resources