Android - onDataChange() strange behavior - android

I create an app with Firebase. There is an issue that i can't solve, and didn't find it talked here.
In this method I want to check if some data is already in the server. If not - I want to add it (the code of adding works well. The Firebase database is being changed as I want). so I'm using onDataChange method as following:
public boolean insertNewList(String title)
{
System.out.println("start: ");
final boolean[] result = new boolean[1];
result[0]=false;
final String group = title;
mRootRef = some reference...
mRootRef.addValueEventListener(new ValueEventListener()
{
#Override
public void onDataChange(DataSnapshot dataSnapshot)
{
System.out.println(0);
if (dataSnapshot.child(group).getValue() != null)
{
System.out.println(group + " is already exist");
System.out.println(1);
}
//write the data.
else
{
mRootRef=mRootRef.child(group);
mRootRef.push().setValue("some data");
System.out.println(2);
result[0]=true;
}
}
#Override
public void onCancelled(DatabaseError databaseError)
{
}
});
System.out.println(3);
return result[0];
}
But what realy happens is this output:
begin:
3 (just skip on onDataChange method and return false).
some print after calling the function
0 (goes back to function and enter to onDataChange method)
2 (finally goes where I want it to go)
0 (for some reason enters twice :( )
1
And because of that i receive wrong results in this function.
Can you help please?

Replace
mRootRef.addValueEventListener(new ValueEventListener()
with
mRootRef.addListenerForSingleValueEvent(new ValueEventListener()
When you add the the value to firebase, "addValueEventListener" called again, not like addListenerForSingleValueEvent that shots only once anywhy.

The output that you showed us looks normal to me. Let me try to explain:
begin: // a) this comes from the System.out.println("begin")
3 // b) you DECLARE your value event listener and then you run
// the System.out.print("3") statement
0 // c) you just added a listener so firebase calls you, and
// your listener fires
2 // d) this is the first time your listener fires, so you go
// into the block called "write the data"
0 // e) since you just wrote the data, firebase calls you again
1 // f) this time, since the data has been written, you go into
// the block called "is alraedy exist"
This is normal behaviour for firebase.
In c), firebase always calls you back one time when you declare a listener.
In e), firebase calls you because the data changed.
But in b), you are only declaring your listener, not yet running it, so the statements after this declaration are executed and you see "3" before anything else happens.

Related

Documents have not changed but considered as added in Firestore Android Studio [duplicate]

I've searched everywhere with no luck. I want to query Firestore to get all users WHERE type is admin. Something like:
SELECT * FROM users WHERE type=admin
but only when the property total is changing. If I'm using:
users.whereEqualTo("type", "admin").addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot snapshots, #Nullable FirebaseFirestoreException e) {
for (DocumentChange dc : snapshots.getDocumentChanges()) {
switch (dc.getType()) {
case ADDED:
//Not trigger
break;
case MODIFIED:
//Trigger
break;
case REMOVED:
//
break;
}
}
}
});
The case ADDED is triggered first time when I query and when the total is changed case MODIFIED is triggered again (this is what is want). I want only changes and not the all initial data, I don't need it. How to get it?
Please help me, is the last part of my project. How to skip is case ADDED?
When you are listening for changes in Cloud Firestore for realtime changes, using Firestore Query's addSnapshotListener() method, it:
Starts listening to this query.
Which basically means that first time you attach the listener, you get all documents that correspond to that particular query. Furthermore, everytime a property within a document changes, you are notified according to that change. Obviously, this is happening only if the listener remains active and is not removed.
Unfortunately, Firestore listeners don't work that way, so you cannot skip that "case ADDED". What you can do instead, is to add add under each user object a Date property (this is how you can add it) and query your database on client, according to this new property, for all documents that have changed since a previous time.
According to Nick Cardoso's comment, for future visitors that might ask why this behaviour happens, is because the reason he mentioned in his comment. I also recommend see Doug Stevenson's answer from this post, for a better understanding.
There is an option to check if the querySnapshot is from a cache, changes return false
if(querySnapshot.getMetadata().isFromCache()) return
Here is a solution working for me:
use
AtomicBoolean isFirstListener = new AtomicBoolean(true);
and then on event method
if (isFirstListener.get()) {
isFirstListener.set(false);
//TODO Handle the entire list.
return;
}
Here is a sample code from my project:
final AtomicBoolean isFirstListener = new AtomicBoolean(true);
mDb.collection("conversation_log").document(room_id).collection("messages").orderBy("sent_at")
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value2, #Nullable FirebaseFirestoreException e) {
if (isFirstListener.get()) {
isFirstListener.set(false);
//TODO Handle the entire list.
return;
}
}
});
reference: answer
Found a work around for this given use case, it is possible to skip the initial data and get only updates. The workaround is including a server timestamp in your structure and build your query to fetch only the data that has timestamp greater than the current time.
val ref = db.collection("Messages").document("Client_ID")
.collection("Private")
.orderBy("timestamp")
.whereGreaterThan("timestamp",Calendar.getInstance().time)
//Add snapshot listener
ref.addSnapshotListener { snapshot , e ->
if (snapshot != null) {
Log.d("TAG", "Current data: ${snapshot.documents.size}")
for(document in snapshot.documents){
Log.e("Document Data",document.data.toString())
}
}
}
So, the query won't return any data in the initial build, but will listen to the document changes. As soon as timestamp of a document changes, you'll be notified about that change. Then you can check if the data exists in your list (if you're looking for modifications, or if it's a new document added)
Just update timestamp of the document when your write any changes. As shown below :
val message = hashMapOf<String,Any>(
"timestamp" to FieldValue.serverTimestamp(),
"id" to userId,
"data" to data,
)
db.collection("Messages").document("Client_ID")
.collection("Private")
.document().set(message)

Why observe MutableLiveData triggers twice without postValue?

I want to add file path to Db and when a file already exists in DB show a Toast message. In ViewModel class:
public void addFile(SharedFile file) {
DefaultExecutorSupplier.getInstance().forBackgroundTasks()
.execute(() -> {
long result = fileRepository.insert(file);
insertResult.postValue(result);
}
);
}
public MutableLiveData<Long> getInsertResult() {
return insertResult;
}
and in the Fragment onViewCreated:
viewModel.getInsertResult().observe(getViewLifecycleOwner(), aLong -> {
if (aLong == -1) {
Toast.makeText(getContext(), getString(R.string.already_exist_file), Toast.LENGTH_LONG).show();
}
});
It works and when I add a repetitive file it Toasts the message, but the problem is when I open another fragment and back to the current fragment it again Toasts message.
This is because when (re)subscribing to a LiveData you always receive the value that was emitted last. See here under Always up to date data. Some ways around this are discussed here: Android LiveData prevent receive the last value on observe

How to ignore or disable the first query snapshot of addSnapshotListener in firestore for android [duplicate]

I've searched everywhere with no luck. I want to query Firestore to get all users WHERE type is admin. Something like:
SELECT * FROM users WHERE type=admin
but only when the property total is changing. If I'm using:
users.whereEqualTo("type", "admin").addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot snapshots, #Nullable FirebaseFirestoreException e) {
for (DocumentChange dc : snapshots.getDocumentChanges()) {
switch (dc.getType()) {
case ADDED:
//Not trigger
break;
case MODIFIED:
//Trigger
break;
case REMOVED:
//
break;
}
}
}
});
The case ADDED is triggered first time when I query and when the total is changed case MODIFIED is triggered again (this is what is want). I want only changes and not the all initial data, I don't need it. How to get it?
Please help me, is the last part of my project. How to skip is case ADDED?
When you are listening for changes in Cloud Firestore for realtime changes, using Firestore Query's addSnapshotListener() method, it:
Starts listening to this query.
Which basically means that first time you attach the listener, you get all documents that correspond to that particular query. Furthermore, everytime a property within a document changes, you are notified according to that change. Obviously, this is happening only if the listener remains active and is not removed.
Unfortunately, Firestore listeners don't work that way, so you cannot skip that "case ADDED". What you can do instead, is to add add under each user object a Date property (this is how you can add it) and query your database on client, according to this new property, for all documents that have changed since a previous time.
According to Nick Cardoso's comment, for future visitors that might ask why this behaviour happens, is because the reason he mentioned in his comment. I also recommend see Doug Stevenson's answer from this post, for a better understanding.
There is an option to check if the querySnapshot is from a cache, changes return false
if(querySnapshot.getMetadata().isFromCache()) return
Here is a solution working for me:
use
AtomicBoolean isFirstListener = new AtomicBoolean(true);
and then on event method
if (isFirstListener.get()) {
isFirstListener.set(false);
//TODO Handle the entire list.
return;
}
Here is a sample code from my project:
final AtomicBoolean isFirstListener = new AtomicBoolean(true);
mDb.collection("conversation_log").document(room_id).collection("messages").orderBy("sent_at")
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value2, #Nullable FirebaseFirestoreException e) {
if (isFirstListener.get()) {
isFirstListener.set(false);
//TODO Handle the entire list.
return;
}
}
});
reference: answer
Found a work around for this given use case, it is possible to skip the initial data and get only updates. The workaround is including a server timestamp in your structure and build your query to fetch only the data that has timestamp greater than the current time.
val ref = db.collection("Messages").document("Client_ID")
.collection("Private")
.orderBy("timestamp")
.whereGreaterThan("timestamp",Calendar.getInstance().time)
//Add snapshot listener
ref.addSnapshotListener { snapshot , e ->
if (snapshot != null) {
Log.d("TAG", "Current data: ${snapshot.documents.size}")
for(document in snapshot.documents){
Log.e("Document Data",document.data.toString())
}
}
}
So, the query won't return any data in the initial build, but will listen to the document changes. As soon as timestamp of a document changes, you'll be notified about that change. Then you can check if the data exists in your list (if you're looking for modifications, or if it's a new document added)
Just update timestamp of the document when your write any changes. As shown below :
val message = hashMapOf<String,Any>(
"timestamp" to FieldValue.serverTimestamp(),
"id" to userId,
"data" to data,
)
db.collection("Messages").document("Client_ID")
.collection("Private")
.document().set(message)

How to stop execution of piece of code in android until data is retrieved from Firebase?

I am trying to perform some task based on the data retrieved from Firebase.
for (inti=0;i<dateList.size();i++)
{
attendanceDateRef= attendanceRef.child(dateList.get(i));
attendanceClassRef= attendanceDateRef.child(ViewAttendanceSelectClassActivity.selectedClass);
attendanceClassRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshotdataSnapshot) {
for (DataSnapshotdsp : dataSnapshot.getChildren())
{
attendanceList.add(dsp.getValue(String.class));
Log.e("attendanceListValues",dsp.getValue(String.class));
}
}
#Override
public void onCancelled(DatabaseErrordatabaseError) {
}
});
}
intindex=1;
for (inti=1;i<=attendanceList.size();i++)
{
if(i%4==0)
{
fullDateRangeList.add(dateList.get(index));
index++;
}
else
{
fullDateRangeList.add(dateList.get(index));
}
}
Log.e("fullDateRangeList",String.valueOf(fullDateRangeList.size()));
Log.e("attendanceList",String.valueOf(attendanceList.size()));
above code is written on the OnClick event of a Button , when I click on the Button following output is generated on logcat:
fullDateRangeList:0
attendanceList:0
attendanceListValues:Value1
attendanceListValues:Value2
attendanceListValues:Value3
attendanceListValues:Value4
.
.
.
attendanceListValues:ValueN
from the above output it looks like second loop is executing before data is retrieved from Firebase and that is why size of fullDateRange and attendanceList is 0.
is there any way i can prevent second loop from executing until data is stored in attendanceList?
You cannot stop that method or make it wait until you get the all the data from your database. This is the behaviour of an asynchronous method. You need to change the logic of your code a little bit by declaring and using that data only inside the onDataChange() method, otherwise it will be always empty.
Also there is another approach. If you want, you can dive into the asynchronous world and use my answer from this post.

Recover Data Firestore Collection

I have a service scheduling screen.
But there is a problem:
I need to check if the date and time the user is trying to schedule is available or reserved.
Structure DB:
Companies
-Company ID (Document)
--name
--phone
---Schedules (Collection)
------Event1
--------Hour: 08:30
--------Date: 01/01/2018
------Event2
--------Hour: 09:00
--------Date: 05/01/2018
------Event3
--------Hour: 10:30
--------Date: 01/002/2018
I access Scheduling data with this code:
String dateExample = "01/01/2018"
String hourExample = "08:30"
FirebaseFirestore mDB = FirebaseFirestore.getInstance();
CollectionReference mDBCompaniesSchedules = (CollectionReference) mDB.collection("Companies").document(mId_Company).collection("Schedules")
.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
for (Schedules document : documentSnapshots.toObjects(Schedules.class)) {
String dtSchedules = document.getSchedules_date();
String hourSchedules = document.getSchedules_hour();
if ( dtSchedules.equals(dateExample) && hourSchedules.equals(hourExample) ){
//Execute a "Toast" and closes the operation
} else {
//Call up the scheduling function.
startScheduling();
}
}
}
})
Process:
I need to run this code and go through all the documents in that collection. I need to check and analyze whether the date and time of the schedule already exists.
If there is: Show a Toast and block.
If there is no: Executing a specific function for the schedule record ("startScheduling()").
Problem:
When the data exists (it will only be once) then it will work.
When there is no data, it falls into the ELSE loop. And it is executing several times the same function "startScheduling();".
I need some way to go through this collection and when I do not find any results, the function "startScheduling (), be executed only once.
This how a for loop works. It will continue iterate till the last element to see if the condition is true or not. With other words, your if-else statement is triggered for every iteration in the loop. It means that, if the condition is true it will go with the if part, if the condition is false it will go with else part, for each and every element.
There are two ways in which you can solve this. One would be to break the loop once the condition was fulfilled. But this means that will iterate till it gets that element. Second, would be to change the logic of your code. Use first the if statement and second iterate.
Edit: The best option in this case would be to query your database using whereEqualTo() method.
Query query = db
.collection("Companies")
.document(mId_Company)
.collection("Schedules")
.whereEqualTo("dtSchedules", dateExample)
.whereEqualTo("hourSchedules", hourExample);
In which dateExample and hourExample are the actual values with which you want to compare.
To count the number of documents in a Collection, please use the following code:
public void onSuccess(QuerySnapshot documentSnapshots) {
if(documentSnapshots.size() == 0) {
startScheduling();
}
for (Schedules document : documentSnapshots.toObjects(Schedules.class)) {
String dtSchedules = document.getSchedules_date();
String hourSchedules = document.getSchedules_hour();
}
}

Categories

Resources