Firestore - Merging two queries locally - java

Since there is no logical OR operator in Firestore, I am trying to merge 2 separate queries locally.
Now I wonder how I can keep up the proper order of the results. When I run 2 queries independently, I can't oder the results specificly (at least not the order in which I get the results from Firestore with the orderBy method).
My idea was to put the 2nd query inside the onSuccessListener of the 1st query. Is this a bad idea performance wise?
public void loadNotes(View v) {
collectionRef.whereLessThan("priority", 2)
.orderBy("priority")
.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
for (QueryDocumentSnapshot documentSnapshot : queryDocumentSnapshots) {
Note note = documentSnapshot.toObject(Note.class);
//adding the results to a List
}
collectionRef.whereGreaterThan("priority", 2)
.orderBy("priority")
.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
for (QueryDocumentSnapshot documentSnapshot : queryDocumentSnapshots) {
Note note = documentSnapshot.toObject(Note.class);
//adding the results to a List
}
}
});
}
});
}

To merge 2 separate queries locally, I recommend you to use Tasks.whenAllSuccess() method. You can achieve this, using the following lines of code:
FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
Query firstQuery = rootRef...
Query secondQuery = rootRef...
Task firstTask = firstQuery.get();
Task secondTask = secondQuery.get();
Task combinedTask = Tasks.whenAllSuccess(firstTask, secondTask).addOnSuccessListener(new OnSuccessListener<List<Object>>() {
#Override
public void onSuccess(List<Object> list) {
//Do what you need to do with your list
}
});
As you can see, when overriding the onSuccess() method the result is a list of objects which has the exact order of the tasks that were passed as arguments into the whenAllSuccess() method.
There is also another approach and that would be to use Tasks.continueWith() method. But according to the use-case of your app, you can use eiter whenAllSuccess() method or continueWith() method. Please see here the official documentation.

Related

How to change an array value with a method call in Android when using Firebase and Android

I have the following method in Android (implemented in Java):
public static void getInformationWhenInfoButtonIsPressed (FirebaseRatingCallback callback,float [] ratingValue, String item) {
DatabaseReference rootRef_Firebase;
rootRef_Firebase = FirebaseDatabase.getInstance(DataBaseEntries.FIREBASE_URL).getReference();
rootRef_Firebase
.child(DataBaseEntries.FIREBASE_NODE_RATINGS_AGGREGATED)
.orderByChild(DataBaseEntries.FIREBASE_NAME)
.equalTo(item)
.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
int numberOfRatingsOverall = 0;
double tempScoreOverall =0;
for (DataSnapshot ds: dataSnapshot.getChildren()) {
float averageRatingOfCurrentDrink = 0;
int numberOfRatingsCurrentDrink = 0;
if (ds.child(DataBaseEntries.FIREBASE_AVERAGE_RATING).getValue(Float.class)!=null) {
averageRatingOfCurrentDrink= ds.child(DataBaseEntries.FIREBASE_AVERAGE_RATING).getValue(Float.class);
}
if (ds.child(DataBaseEntries.FIREBASE_NUMBER_OF_RATINGS).getValue(Integer.class)!=null) {
numberOfRatingsCurrentDrink = ds.child(DataBaseEntries.FIREBASE_NUMBER_OF_RATINGS).getValue(Integer.class);
numberOfRatingsOverall= numberOfRatingsOverall + numberOfRatingsCurrentDrink ;
}
tempScoreOverall = tempScoreOverall + numberOfRatingsCurrentDrink * averageRatingOfCurrentDrink;
}
ratingValue[0] = (float) (tempScoreOverall/numberOfRatingsOverall);
}
#Override
public void onCancelled(#NonNull DatabaseError error) {
throw error.toException();
}
});
ratingValue[0] = (float) (Math.round(ratingValue[0] * 10.0) / 10.0);
}
And within the same class I have the following interface:
public interface FirebaseRatingCallback {
void onCallBackRating (boolean dataIsReadFromFirebase); }
The method should change the value of the reference data type array ratingValue[0]. Unfortunately Firebase Realtime database works asynchronously so I have to use a callback which I do using FirebaseRatingCallback callback. But I don't know how to do this using a ValueEventListener as I am doing in this example. I know how to do this with a OnCompleteListener because there you have the method public void onComplete(#NonNull Task<Void> task) { which I can't use when having a ValueEventListener.
So I tried to add the following but this does not work:
public static void getInformationWhenInfoButtonIsPressed (FirebaseRatingCallback callback,float [] ratingValue, String item) {
DatabaseReference rootRef_Firebase;
rootRef_Firebase = FirebaseDatabase.getInstance(DataBaseEntries.FIREBASE_URL).getReference();
rootRef_Firebase
.child(DataBaseEntries.FIREBASE_NODE_RATINGS_AGGREGATED)
.orderByChild(DataBaseEntries.FIREBASE_NAME)
.equalTo(item)
.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
int numberOfRatingsOverall = 0;
double tempScoreOverall =0;
for (DataSnapshot ds: dataSnapshot.getChildren()) {
float averageRatingOfCurrentDrink = 0;
int numberOfRatingsCurrentDrink = 0;
if (ds.child(DataBaseEntries.FIREBASE_AVERAGE_RATING).getValue(Float.class)!=null) {
averageRatingOfCurrentDrink= ds.child(DataBaseEntries.FIREBASE_AVERAGE_RATING).getValue(Float.class);
}
if (ds.child(DataBaseEntries.FIREBASE_NUMBER_OF_RATINGS).getValue(Integer.class)!=null) {
numberOfRatingsCurrentDrink = ds.child(DataBaseEntries.FIREBASE_NUMBER_OF_RATINGS).getValue(Integer.class);
numberOfRatingsOverall= numberOfRatingsOverall + numberOfRatingsCurrentDrink ;
}
tempScoreOverall = tempScoreOverall + numberOfRatingsCurrentDrink * averageRatingOfCurrentDrink;
}
public void onComplete(#NonNull Task<Void> task) {
ratingValue[0] = (float) (tempScoreOverall/numberOfRatingsOverall);
callback.onCallBackRating(true);
}
}
#Override
public void onCancelled(#NonNull DatabaseError error) {
throw error.toException();
}
});
}
Does anyone know how to do this?
I know how to do this when writing into the database, but I don't know how to do this when reading something out of it.
The simplest solution would be to call get() on a query object, and once the operation is complete, update the LiveData object. After that, simply observe the change in the UI.
From the documentation of Query#get():
Executes the query and returns the results as a QuerySnapshot.
So once the query is complete, there will be no other future events, even if the database changes. So there will be nothing that will affect your UI after the query is complete.
Edit:
Is it not possible to do this without LiveData?
There are other methods to do that, but this is by far the simplest one. Callbacks are not recommended to be used anymore.
When using LiveData I need a lot of code because I'd also need 2 additional classes (ViewModel and LiveData class).
What's wrong with that? That's programming. Regarding, a ViewModel that's the way the Android team recommends building the apps.
not do something completely new with LiveData and ViewModel as this would also take too much time and increase the complexity because of the many classes.
It won't take much time. It will take a matter of minutes. As soon as you understand those concepts, you'll be a better Android developer. Don't be afraid to learn something new.
P.S. Java is still a great language, but for Android development, I recommend Kotlin. What you are trying to achieve can be written in Kotlin using a single line of code.

Using variable from onComplete() method while working with Firestore database

So I have a collection in Cloud Firestore and I need to store the informations from database in a list (coordPlaces). I did it like this, created these methods:
private void readData(FirestoreCallback firestoreCallback){
db.collection("AddPlaces")
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if(task.isSuccessful()){
if(task.getResult() != null){
List<ListPlacesModel> list = task.getResult().toObjects(ListPlacesModel.class);
for(ListPlacesModel model : list)
{
LatLng place = new LatLng(model.getPlaceLat(), model.getPlaceLng());
coordPlaces.add(place);
String name = model.getPlaceName();
gMap.addMarker(new MarkerOptions().position(place).title(name));
}
firestoreCallback.onCallback(coordPlaces);
}
}
else {
Log.d("ERROR", String.valueOf(task.getException()));
}
}
});
}
private interface FirestoreCallback{
void onCallback(List<LatLng> myList);
}
And then called the method like this:
db = FirebaseFirestore.getInstance();
readData(new FirestoreCallback() {
#Override
public void onCallback(List<LatLng> myList) {
});
I've saw this solution in this video.
But now I need to access this list in order to creathe a 2D array out of it, in this manner:
for(int i=0;i<coordPlaces.size();i++)
{
for(int j=0;j<coordPlaces.size();j++)
{
int distance = (int) SphericalUtil.computeDistanceBetween(coordPlaces.get(i), coordPlaces.get(i+1));
graph[i][j] = distance;
}
}
Anywhere in the code where I put these 2 for loops, my app crashes. I can't find a solution of how to create this 2D array.
Any ideas?
The only places where you should add those lines of code should be either inside onComplete() or if you're using a custom callback inside onCallback(). If you place those lines in any other parts of your class, you'll get an empty list, hence that IndexOutOfBoundsException.
If you don't want to use that approach, you might also consider using a LiveData object, as it's a type of object that can be observed. If you understand Kotlin, the following resource will help:
How to read data from Cloud Firestore using get()?

Return called before reaching `return` statement: Java-Android [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

Synchronization problem while getting data from firebase database

I have a problem with my Firebase database data retrieve sync. I am able to get data but return works before it. So my data never put in the intended list.
Here is my code:
DatabaseManager databaseManager = new DatabaseManager();
MedicineData medicineData = new MedicineData();
boolean validated = false;
private static final String TAG = "BarcodeDecoderDataMatrix";
public Map getDataMatrix(String dataMatrixText) {
Map<String, Object> dataMatrix = new HashMap<>();
String barcodeNum = getBarcodeNumber(dataMatrixText);
String expireDate = getExpireDate(dataMatrixText);
String serialNum = getSerialNumber(dataMatrixText);
String partyNum = getPartyNumber(dataMatrixText);
dataMatrix.put("barcodeNumber",barcodeNum);
dataMatrix.put("expireDate", expireDate);
dataMatrix.put("serialNumber", serialNum);
dataMatrix.put("partyNumber", partyNum);
getMedicineName(barcodeNum, (success) -> {
if(success){
//find the data on database
dataMatrix.put("medicineName", medicineData.getProductName());
dataMatrix.put("companyName", medicineData.getCompanyName());
dataMatrix.put("price", medicineData.getPrice());
}
else {
//can't find on database
}
});
return dataMatrix;
}
This method called from another class to get dataMatrix list.
private void getMedicineName(String barcodeNum, SimpleCallback<Boolean> finishedCallback) {
DatabaseReference rootRef = databaseManager.getReference();
DatabaseReference medicinesRef = rootRef.child("MedicineList");
Query queryMedicineNameFinder = medicinesRef.orderByKey().equalTo(barcodeNum);
ValueEventListener valueEventListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for(DataSnapshot ds : dataSnapshot.getChildren()) {
medicineData = ds.getValue(MedicineData.class);
}
if (medicineData != null){
validated = true;
}
finishedCallback.run(validated);
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
Log.d(TAG, databaseError.getMessage()); //Don't ignore errors!
finishedCallback.run(false);
}
};
queryMedicineNameFinder.addListenerForSingleValueEvent(valueEventListener);
}
These two methods are in the same class called BarcodeDecoderDataMatrix. I create an instance of this class from another class and call getDataMatrix method. I am expecting to get a list include my values. I can get barcodeNum, expireDate, serialNum, partyNum values without a problem. But the list doesn't include medicineName, companyName and price informations. I made a debug so I know I can get data from database. I can see it in medicineData variable. I am pretty sure it is a sync issue. Because my data in medicineData but it can't put it in the list before return called.
How can I make this happen?
addListenerForSingleValueEvent() method that you are using in your code, is asynchronous. This means that it returns immediately, while onDataChange() method fires only when the results are available.
What you are doing in your code is incorrect because you assuming that the results are immediately available and are not.
When you call getMedicineName() method, you should directly use dataMatrix in the callback, after the data is known that is available.
For more information, 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.

Is there a way to paginate queries by combining query cursors using FirestoreRecyclerAdapter?

I have a query that looks like this:
Query first = ref.orderBy("name", Query.Direction.ASCENDING).limit(10);
This is how I display the data to my RecyclerView.
firestoreRecyclerOptions = new FirestoreRecyclerOptions.Builder<ModelClass>().setQuery(query, ModelClass.class).build();
myFirestoreRecyclerAdapter = new MyFirestoreRecyclerAdapter(firestoreRecyclerOptions);
recyclerView.setAdapter(myFirestoreRecyclerAdapter);
I'm trying to use pagination as specified in here and I cannot solve it.
first.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
DocumentSnapshot lastVisible = documentSnapshots.getDocuments().get(documentSnapshots.size() - 1);
Query second = ref.orderBy("name", Query.Direction.ASCENDING).startAfter(lastVisible).limit(10);
//Create a firestoreRecyclerOptions and setting the adapter
}
});
Is there a way to paginate queries by combining query cursors using FirestoreRecyclerAdapter? Is this even possible?
As #FrankvanPuffelen already answered in an earlier question of yours, you cannot achieve that because in your case, you should pass 2 different queries (first and second) to a single adapter, which is not possible with FirestoreRecyclerAdapter. You can either use the first query or the second with a single instance of your adapter.
A solution would be to create two different lists, containing the results from each query and combine them. Then you can pass the resulting list to another adapter, let's say an ArrayAdapter and then display the results into a ListView or even better in a RecyclerView. The problem in this case is that you will not be able to use the real-time features that the FirestoreRecyclerAdapter class provides but this approach will solve your problem.
Edit:
According to your request from the comment section, I'll give you an example on how to paginate a query on button click in the easiest way, using a ListView and an ArrayAdapter. You can achieve the same thing also using a RecyclerView when scrolling down. But to keep things simple, let's assume we have a ListView and a Button and we want to load more items to the list on every button click. For that, let's define first the views :
ListView listView = findViewById(R.id.list_view);
Button button = findViewById(R.id.button);
Let's assume we have a database structure that looks like this:
Firestore-root
|
--- products (collection)
|
--- productId (document)
|
--- productName: "Product Name"
And a model class that looks like this:
public class ProductModel {
private String productName;
public ProductModel() {}
public ProductModel(String productName) {this.productName = productName;}
public String getProductName() {return productName;}
#Override
public String toString() { return productName; }
}
Now, let's define a query with the limit set to 3.
FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
CollectionReference productsRef = rootRef.collection("products");
Query firstQuery = productsRef.orderBy("productName", Query.Direction.ASCENDING).limit(3);
This means that on every button click, we'll load 3 more items. And now, here is the code that does the magic:
firstQuery.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<ProductModel> list = new ArrayList<>();
for (DocumentSnapshot document : task.getResult()) {
ProductModel productModel = document.toObject(ProductModel.class);
list.add(productModel);
}
ArrayAdapter<ProductModel> arrayAdapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, list);
listView.setAdapter(arrayAdapter);
lastVisible = task.getResult().getDocuments().get(task.getResult().size() - 1);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Query nextQuery = productsRef.orderBy("productName", Query.Direction.ASCENDING).startAfter(lastVisible).limit(3);
nextQuery.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> t) {
if (t.isSuccessful()) {
for (DocumentSnapshot d : t.getResult()) {
ProductModel productModel = d.toObject(ProductModel.class);
list.add(productModel);
}
arrayAdapter.notifyDataSetChanged();
lastVisible = t.getResult().getDocuments().get(t.getResult().size() - 1);
}
}
});
}
});
}
}
});
In which lastVisible is a DocumentSnapshot object that represents the last visibile item from the query. In this case, every third one and it is declared as gloabl variable:
private DocumentSnapshot lastVisible;
Edit2:
Here you have also the solution on how you can get the data from your Firestore database and display it in smaller chunks in a RecyclerView when user scrolls.
A possible approach is save the push keys of each item in a separate node.
Then fetch all keys from that node to an array list or something ..
Then based on your pagination number fetch the keys from this array List and use orderByKey Query and startAt and LimitToFirst queries combined to make the pagination algorithm

Categories

Resources