reading Firestore documents with AsyncTask - java

I have a database in Cloud Firestore where each document has a particular key "last update" with value, a String, representing a date in the form YYYY-MM-DD. Each time a document is updated, the value of "last update" is set as the date of the update.
Now, I want my activity to have a method that checks documents for their last update. As the documents contain fairly big lists of objects, this update check takes a few seconds. So I decided to defer it to an AsyncTask. The doInBackground method of the AsyncTask should create a DocumentReference, noteRef, for the document and read its "last update" with noteRef.get(), equipped with
onSuccess- and onFailure listeners, into a String, which is then returned by the method.
In order to test this, I have created a toy activity, MyTestActivity, which calls the above AsyncTask with String arguments "myCollection" and "myDocument" and
displays the value of this document's last update in a TextView. Now, instead of showing the actual value, "2019-10-03", the TextView displays the value, "1970-01-01", which is the one
used in doInBackground to initialize the String variable which is returned. It's as if doInBackground doesn't bother to wait until the document has been read. The code is as follows.
public class MyTestActivity extends AppCompatActivity{
private Button button;
private TextView textView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_test);
button = findViewById(R.id.update_button);
textView = findViewById(R.id.update_text_view);
}
public void buttonClicked(View view) throws ExecutionException, InterruptedException {
UpdateTask task = new UpdateTask(this, "myCollection", "myDocument");
String date = task.execute().get();
textView.setText("Last update on "+date);
}
private static class UpdateTask extends AsyncTask<Integer, Integer, String> {
private WeakReference<MyTestActivity> activityWeakReference;
String collection;
String document;
String lastUpdate;
UpdateTask(MyTestActivity activity, String collection, String document) {
activityWeakReference = new WeakReference<MyTestActivity>(activity);
this.collection = collection;
this.document = document;
lastUpdate = new String();
}
#Override
protected void onPreExecute() {
super.onPreExecute();
MyTestActivity activity = activityWeakReference.get();
if (activity == null || activity.isFinishing()) {
return;
}
}
#Override
protected String doInBackground(Integer... params) {
FirebaseFirestore db = FirebaseFirestore.getInstance();
DocumentReference noteRef = db.collection(collection).document(document);
lastUpdate = "1970-01-01";
noteRef.get()
.addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists()) {
Map<String, Object> map = documentSnapshot.getData();
lastUpdate = (String)map.get("last update");
activityWeakReference.get().textView.setText(lastUpdate);
} else {
lastUpdate = "Document doesn't exist";
}
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
lastUpdate = "Listener failed.";
}
});
return lastUpdate;
}
}
}
Can anyone explain what is going on here?

I have a database in Firebase Firestore where each document has a particular key "last update" with value, a String, representing a date in the form YYYY-MM-DD.
That's uncommon to store the date as a String, instead you should store it as a:
FieldValue.serverTimestamp()
As explained in my answer from the following post:
ServerTimestamp is always null on Firebase Firestore
So I decided to defer it to an AsyncTask.
The Cloud Firestore database client, already runs all network operations in a background thread. This means that all operations take place without blocking your main thread. Adding it in an AsyncTask does not give any any benefits at all.
Now, instead of showing the actual value, "2019-10-03", the TextView displays the value, "1970-01-01", which is the one used in doInBackground
This is happening because you are trying to return a message synchronously from a method that is asynchronous. That's not a good idea. You should handle the APIs asynchronously as intended.
A quick solve for this problem would be to use value of lastUpdate only inside the onSuccess() method, otherwise 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.

Ouch! That's bad design. Firestore calls are asynchronous, so you don't need to put them into asyncTask background method. Also, using an synctask wont execute your code faster. What you need is a "loading message" until your OnSuccessListener fires back.

Related

Callback in Firestore still returning null values from database

After my last question regarding reading data from a Firestore collection, I had to create a callback function in order to use the asynchronized data that Firestore sends. However, even with the callback function created, security rules in Firestore console overruled and a guarantee that there is a connection between my app and the database, I still get a null value.
Here is my callback interface
public interface Callback {
void myResponseCallback(String result);
}
And here is my updated code to work with this callback:
FirebaseFirestore db = FirebaseFirestore.getInstance();
DocumentReference docRef =
db.collection("usuarios")
.document("idtest");
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_menu);
tvMenu = findViewById(R.id.tvMainMenu);
btOpenOs = findViewById(R.id.btNewOs);
}
public void readDataFromFirestore(Callback callback){
//The app doesn't even enter this part of the code, as if they stop right at the first IF
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document.exists()) {
dbValue = document.getString("palavra");
//tvMenu.setText(dbValue); DOESN'T WORK
Toast.makeText(MenuActivity.this, "String Value: " + document.getString("palavra"), Toast.LENGTH_SHORT).show();
}
}
callback.myResponseCallback(dbValue);
}
});
}
public void setDataFromFirestore() {
readDataFromFirestore(new Callback() {
#Override
public void myResponseCallback(String result) {
tvMenu.setText(result);
//Returns null
Toast.makeText(MenuActivity.this, "Result: " + result, Toast.LENGTH_SHORT).show();
}
});
}
public void openOsActivity(View view) {
/* Intent osIntent = new Intent(this, OsActivity.class);
startActivity(osIntent);*/
setDataFromFirestore();
}
The Toast from setDataFromFirestore() returns a "Result: null". Here's a picture of my FireStore collection of which I'm trying to read the field "palavra":
What exactly am I doing wrong here? It's my first time with Firestore and even after reading the documentation and other SO posts, I'm stuck. Any help would be greatly appreciated.
EDIT
Running the debugger, it seems that the result is also null. Here is a image from it:
However I could use some help understanding some of the information in there.
The problem as Frank van Puffelen described was that my code was loading "idtest" instead of idteste, as it is in my firestore database. Apart from that, it seems that the callback function made all of this possible.

Updating a part of the RecyclerView

i have a recycle view that contains
a description of a comment and a vote count when i press up i need the count to increase this is how it looks like at the moment
but the problem is when i press up the vote does increase but until i close that fragment and reopen it it does not update
what actually happens is when i press up vote it makes an API call which does some logic and sends it over to android which has a number of the vote for that particular item which was being clicked or upvoted
how can i update that particular record so that i can see the change without reopening the view
code:
CommentItem.java
public CommentItem(String id, String description, String voteCount)
{
this.id = id;
this.description= description;
this.voteCount = voteCount;
}
//getters and setters
CommentItemAdapter.java
...
private OnItemClickListener mlistener;
/*Interfaces*/
public interface OnItemClickListener{
void VoteUpClick(int position);
void VoteDownClick(int position);
...
}
...
CommentFragment.java
#Override
public void VoteUpClick(final int position) {
final CommentItem clickeedItem =
mitemList.get(position);
StringRequest strReq = new StringRequest(Request.Method.POST,
AppConfig.URL, new Response.Listener<String>() {
#Override
public void onResponse(String response) {
try {
JSONObject jObj = new JSONObject(response);
boolean error = jObj.getBoolean("error");
if (!error) {
String errorMsg = jObj.getString("msg");
Toast.makeText(getContext(), errorMsg,
Toast.LENGTH_SHORT).show();
} else {
String msg= jObj.getString("msg");
Toast.makeText(getContext(), msg,
Toast.LENGTH_SHORT).show();
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {}
}) {
#Override
protected Map<String, String> getParams() {
Map<String, String> params = new HashMap<String, String>();
params.put("voteType", "up");
params.put("commentID", clickeedItem.getId().toString());
return params;
}
};
AppController.getInstance().addToRequestQueue(strReq, tag_string_req);
}
If I get it right, simply adapter.notifyDataSetChanged() will update your list. But There is a better way, using DiffUtils in recyclerView. That is, comparing old and new list, and updating only the changed item, not changing the whole data set.
Have a look at it out
https://medium.com/#iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00
Other approach, is to have the viewHolder reference from the Activity/Or Fragment, when onItemClick happens, by recyclerView.findViewHolderForAdapterPosition(position). Then change view by this view
A lot of people here had a nice advice to use DiffUtils. Thats quite a reasonable way to handle diffs inside dataset of RecycleView.
The best way to perform it right now is to use ListAdapter, which requires a DiffUtil.ItemCallback or AsyncDifferConfig. A really big pros of ListAdapter is that all differ logic is done on background, which in turns optimizes your UI.
Everything you need is to override this funs: areItemsTheSame() and areContentsTheSame(), additionally u have getChangePayload() - for detailed info about the changed item.
Don't use the notifyDataSetChanged() and other range update functions, all of that stuff is handled under-the-hood.
Yours case could be handled via different approaches. I prefer having an intermediate state which will notify user that something happens. So you can locally mark that comment with pending vote up, for example yellow arrow and when response is obtained from back-end you just need to refresh the data-list and the ItemCallback will do the diff trick for you. When response is retrieved and vote is applied it can be marked as green arrow. Those are just thoughts about the right flow.
In any case everything you need is to use the ListAdapter.sumbitList(newDataSet) and the internal differ of ListAdapter will use ItemCallback to compare old and new list.

How to synchronously query data from room database?

This is my first time doing android dev. Sorry for my lack of knowledge in..well everything.
I am trying to query some data on the main thread of an activity using asynctask. The problem is, the data that I queried is needed immediately in some other query of data, so the asynchronous nature of the query means that every time I need to use the data, the thread has not queried it yet and gives a nullpointer exception.
Is there a way to synchronously query data from room database?
I tried the getValue() function from a LiveData object, but it always returns null as well. I am sure that the data is inserted properly within the database, I have checked multiple times looking into the database while debugging.
This is the code I used to query an entity of Day class:
//load current day
findSpecificDayAsyncTask asyncTask = (findSpecificDayAsyncTask) new findSpecificDayAsyncTask(mDayDao, new findSpecificDayAsyncTask.AsyncResponse() {
#Override
public void processFinish(Day output) {
day1 = output;
}
}).execute(date);
It works in due time, but I need the data immediately so that I can query
some other data:
mBPViewModel = ViewModelProviders.of(this).get(BulletPointViewModel.class);
//the day1 class is used here as a parameter
mBPViewModel.getSpecificDayBulletPoints(day1.day).observe(this, new Observer<List<BulletPoint>>() {
#Override
public void onChanged(#Nullable final List<BulletPoint> bulletPoints) {
// Update the cached copy of the words in the adapter.
mAdapter.setBulletPoints(bulletPoints);
}
});
So is there a way for me to synchronously query data so I don't get
a nullpointer exception?
Why not doing like this
//load current day
findSpecificDayAsyncTask asyncTask = (findSpecificDayAsyncTask) new
findSpecificDayAsyncTask(mDayDao, new findSpecificDayAsyncTask.AsyncResponse() {
#Override
public void processFinish(Day output) {
day1 = output;
mBPViewModel = ViewModelProviders.of(this).get(BulletPointViewModel.class);
//the day1 class is used here as a parameter
mBPViewModel.getSpecificDayBulletPoints(day1.day).observe(this, new Observer<List<BulletPoint>>() {
#Override
public void onChanged(#Nullable final List<BulletPoint> bulletPoints) {
// Update the cached copy of the words in the adapter.
mAdapter.setBulletPoints(bulletPoints);
}
});
}
}).execute(date);

Problems with Firestore connections executing threads

First of all, I would like to apologize if the title is misleading. English is not my native language and I wasn't sure how to name this post. Now the question:
I have an Activity that shows the data stored in a Firebase project about a user. The data is distributed between the Firebase user (display name, email and profile image) and a document in Cloud Firestore named as the user's UID.
When this activity starts, I make a Firebase google auth to get the user, and then the problems come. I need to know if the user has a linked document in the database with his additional data (existing user) or if he needs to create one (new user). I have created a method that checks if a document named like the user's UID exists. This is the method:
public void userExists(String uid) {
FirebaseFirestore db = FirebaseFirestore.getInstance();
DocumentReference docRef = db.collection("users").document(uid);
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot documentSnapshot = task.getResult();
if (documentSnapshot.exists()) {
aa = true;
aa=true;
} else {
aa = false;
}
} else {
aa = false;
}
}
});
}
(aa is a boolean variable declared in the Activity).
I call this method inside the following one in order to know if I need to start a new Activity to create the document or if I can show the user's data in the current Activity without problems.
private void updateUI(FirebaseUser user) {
if (user != null) {
userExists(user.getUid());
if(aa){
//Fill layout with the user data and the user linked document data
//USER DATA
txvNombre=findViewById(R.id.nombrePerfil);
txvNombre.setText(user.getDisplayName());
imvAvatar=findViewById(R.id.imvVistaPerfilAvatar);
Picasso.with(VistaPerfilActivity.this)
.load(user.getPhotoUrl())
.resize(500,500)
.centerCrop()
.into(imvAvatar);
//HERE GOES THE DOCUMENT DATA
}else{
}
} else {
finish();
}
}
As far as I know, Firestore connections are made in a new Thread so, when UpdateUI(FirebaseUser user) starts, aa is always false, because userExists(String uid) hasn't finished yet. userExists(String uid) works correctly, I have checked it.
So I need to know how to check if the Firestore connection thread is finished, in order to continue executing the app. I have tried using the OnCompleteListener (shown in the code), but it doesn't work. I've also tried to just write the actions in the userExists(String uid) method instead of just changing the value of aa and then continue on another method, but I get the
variable is accessed from within inner class needs to be declared final
error. I tried to follow the Android Studio advice of making the variable final, but I can't work with that for obvious reasons.
Thanks in advance.
The problem is not so much caused by multi-thread, as by the fact that data is loaded from Firebase asynchronously. By the time your updateUI function looks at the value of aa, the onComplete hasn't run yet.
This is easiest to see by placing a few well placed logging statements:
System.out.println("Before attaching listener");
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
System.out.println("Got document");
}
});
System.out.println("After attaching listener");
When you run this code it prints
Before attaching listener
After attaching listener
Got document
This is probably not what you expected, but it explains precisely why aa is unmodified when updateUI checks it. The document hasn't been read from Firestore yet, so onComplete hasn't run yet.
The solution for this is to move all code that requires data from the database into the onComplete method. The simplest way in your case is:
public void userExists(String uid) {
FirebaseFirestore db = FirebaseFirestore.getInstance();
DocumentReference docRef = db.collection("users").document(uid);
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot documentSnapshot = task.getResult();
if (documentSnapshot.exists()) {
//Fill layout with the user data and the user linked document data
//USER DATA
txvNombre=findViewById(R.id.nombrePerfil);
txvNombre.setText(user.getDisplayName());
imvAvatar=findViewById(R.id.imvVistaPerfilAvatar);
Picasso.with(VistaPerfilActivity.this)
.load(user.getPhotoUrl())
.resize(500,500)
.centerCrop()
.into(imvAvatar);
//HERE GOES THE DOCUMENT DATA
}
}
}
});
}
Now your code that needs the document only runs after the document is actually available. This will work, but it does make the userExists function a bit less reusable. If you want to fix that, you can pass a callback into userExists that you then call after the document is loaded.
public interface UserExistsCallback {
void onCallback(boolean isExisting);
}
And use that in userExists as:
public void userExists(String uid, final UserExistsCallback callback) {
FirebaseFirestore db = FirebaseFirestore.getInstance();
DocumentReference docRef = db.collection("users").document(uid);
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
boolean userExists = false;
if (task.isSuccessful()) {
DocumentSnapshot documentSnapshot = task.getResult();
userExists = documentSnapshot.exists();
}
callback.onCallback(userExists);
}
});
}
And then invoke that from updateUI with:
if (user != null) {
userExists(user.getUid(), new UserExistsCallback() {
public void onCallback(boolean isExisting) {
if(isExisting){
//Fill layout with the user data and the user linked document data
//USER DATA
txvNombre=findViewById(R.id.nombrePerfil);
txvNombre.setText(user.getDisplayName());
imvAvatar=findViewById(R.id.imvVistaPerfilAvatar);
Picasso.with(VistaPerfilActivity.this)
.load(user.getPhotoUrl())
.resize(500,500)
.centerCrop()
.into(imvAvatar);
//HERE GOES THE DOCUMENT DATA
}else{
}
} else {
finish();
}
});
}
As you can see our UserExistsCallback is quite similar to the OnCompleteListener of Firestore itself, it's just a bit more tailored to our needs.
This problem pops up a lot, so I recommend spending some time learning more about it. See:
get all table values from firebase null object reference firebase database
Setting Singleton property value in Firebase Listener
Regarding the final reference, if the variable belongs to a class rather than being declared in the method, it need not be declared final.
You're right guessing that, when using the value of your aa variable outside the onComplete() method, the data hasn't finished loading yet from the database and that's why it's not accessible. So this variable will aways hold the initial value of false.
So in order to solve this, you need to wait for it. This can be done in two ways. The first solution would be a very quick solution that implies you to use the value of your aa variable only inside the onComplete() method and it will work perfectly fine. The second one is, if you want to use it outside the onComplete() method, I recommend you to 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.

Firebase - retrieving data by value to an outside variable

I have a question regarding Firebase Realtime database.
I'm trying to do a bookmark option in my program, which allows the user to store his/her's favourite pages, however whenever I try to retrieve data from my firebase database, the data is restored after the method returns a value.
public static boolean checkIfBookmarked(final String title){
final FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
final DatabaseReference userBookmarks = FirebaseDatabase.getInstance().getReference().child("users")
.child(user.getUid()).child("bookmarks");
final boolean[] exists = new boolean[1];
userBookmarks.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
exists[0] = dataSnapshot.child(title).exists() ;
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
return exists[0];
}
Firebase use callback methods to get the data from the server, In your case the return statement will be executed before the callback come from the Firbase. You can try to pass a callback method to your function and execute that when the callback from Firebase is triggered.
public static void checkIfBookmarked(final String title, callbackFunction){
final FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
final DatabaseReference userBookmarks = FirebaseDatabase.getInstance().getReference().child("users")
.child(user.getUid()).child("bookmarks");
final boolean[] exists = new boolean[1];
userBookmarks.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
exists[0] = dataSnapshot.child(title).exists() ;
//execute your callback function here
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
return;
}
Check this to see how to pass a function as parameter.
Passing function as a parameter in java
An alternative would be to move your code into OnDataChange method
You cannot return something now that hasn't been loaded yet. With other words, you cannot simply return the first element of your array exists[0], outside the onDataChange() 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 that result 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 exists[0] only inside the onDataChange() method, or if you want to use it outside, I recommend you dive into the asynchronous world and 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.

Categories