I need some help with async tasks and Firebase. I need to save n itens on Firebase Realtime database, but I don't know how to deal with the callback.
Here's my code:
TagRepository tagRepository = new TagRepository();
for (Tag tag : databaseTags){
if (user.getTags().containsKey(tag.getId())){
Completable completable = tagRepository.saveUserOnTag(String.valueOf(tag.getId()), user.getUid());
}else{
tagRepository.removeUserOnTag(String.valueOf(tag.getId()), user.getUid());
}
}
public Completable saveUserOnTag(String idTag, String userUid) {
return io.reactivex.Completable.create(emitter->{
reference.child(idTag).child("users/").child(userUid).setValue(true).addOnCompleteListener(task -> emitter.onComplete());
});
}
If I use an callback on this method, the callback will be called n times, so I don't have any idea how to know when all of them are already saved so I can proceed.
I was trying something with Completable as you can see on the method, but I really don't know how to deal with it. There is any easy way to save all data at same time or to control all data that are being saved??
According to the official documentation, you can use simultaneous updates.
Using those paths, you can perform simultaneous updates to multiple locations in the JSON tree with a single call to updateChildren(). Simultaneous updates made this way are atomic: either all updates succeed or all updates fail.
You could try updating firebase data synchronously using Tasks.await
Change the saveUserOnTag method as follows and try
public Completable saveUserOnTag (String idTag, String userUid){
return io.reactivex.Completable.create(emitter -> {
Tasks.await(reference.child(idTag).child("users/").child(userUid).setValue(true));
emitter.onComplete();
});
Sorry I don't know Rx, but here is a way to insert all the data in one API call. The onComplete() method is the update complete callback.
Map<String, Object> values = new HashMap<>();
for (Tag tag : databaseTags){
if (user.getTags().containsKey(tag.getId())){
values.put(idTag + "/users/" + userUid, true);
}else{
tagRepository.removeUserOnTag(String.valueOf(tag.getId()), user.getUid());
}
}
reference.updateChildren(values, new DatabaseReference.CompletionListener() {
#Override
public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
// all data saved, do next action
}
});
Hope this helps :)
Related
I have a list of string and this list contains node names in Firebase database.
And I have a custom sort method therefore I want to get the all of the keys for each node in my custom list without using Firebase` query sorting functions.
And because of it I need all data to be retrieved, so adding to list as it retrieves some data is not an option.
However the problem starts with asynchronous structure of Firebase.
I iterate through every node name in my custom list and under that loop I create another loop in Firebase` thread ( addListenerForSingleValueEvent ) to retrieve all keys.
But it works asynchronously. I've tried to change it to synchronous by using Thread and Semaphore but it didn't work.
I've also used custom interface (FirebaseDataRetrieveListener) to indicate when the loop in valueEventListener finishes but since valueEventListener instantly return this is not possible without pausing the thread.
If Task's can be used in this situation, how it could be, or are there any other solutions?
private void getKeysFromNodeList(final FirebaseDataRetrieveListener listener) //TODO [BUG] when first for loop initializes it ignores the seperate thread in the inner anonymous class so it'll be already finished it's cycle before Firebase` loop starts...
{
listener.onStart();
final DatabaseReference databaseRef = firebaseInstance.rootRef.child(context.getString(R.string.databaseref));
for (iterator = 0; iterator < nodeList.size(); iterator ++)
{
Query query = databaseRed.child(nodeList.get(iterator));
query.addListenerForSingleValueEvent(new ValueEventListener()
{
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot)
{
for (DataSnapshot snap : dataSnapshot.getChildren())
{
iterator++;
String key= snap.getKey();
// I've done my jobs with given key here
// if got the last key from node and iterated the last node
if (firebaseIterator== dataSnapshot.getChildrenCount() - 1 && iterator == nodeList.size() - 1)
{
firebaseIterator= 0;// reset the iterators
iterator= 0;
listener.onSuccess();
break;
}
// if got the last key from node
else if (firebaseIterator== dataSnapshot.getChildrenCount() - 1)
{
firebaseIterator= 0; // reset the iterator
break;
}
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError)
{
listener.onFailed(databaseError);
return;
}
});
}
;
}
}
FirebaseDataRetrieveListener.java
public interface FirebaseDataRetrieveListener
{
public void onStart();
public void onSuccess();
public void onFailed(DatabaseError databaseError);
}
Basically, you're trying to return a value synchronously from an API that's asynchronous. That's not a good idea. You should handle the APIs asynchronously as intended. Pausing a thread might solve your problem but only partial. and it's not recommened at all.
Firebase APIs are asynchronous, meaning that onDataChange() method returns immediately after it's invoked, and the callback from the Task it returns, will be called some time later. There are no guarantees about how long it will take. So it may take from a few hundred milliseconds to a few seconds before that data is available. Because that method returns immediately, the values that are coming from the database will not have been populated from the callback yet.
A quick solve for this problem would be to use those values only inside the callback, 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.
As a conclusion, there is no way to turn an asynchronously API as Firebase is into a synchronous one.
To solve this problem I made my method recursive and used a listener.
In my solution we are gonna keep track on our how many times we called the recursive method, and how many time Firebase looped through to keys of given node in order to detect whether we reach to end of node and we looped all the nodes in the list.
Declare 2 iterator and 1 listener in your class` scope eg.
private RecursiveListener listener;
private long firebaseIterator = 0;
private int recursiveIterator = 0;
public interface getKeysFromNodeListListener
{
void onFinished();
}
We are gonna use the recursive listener in order to get notified whether our recursive function has finished.
Before calling your method define the listener and override onFinished().
onFinished gonna be called when we both looped through all nodes and their keys.
private void getKeysFromNodeList(final getKeysFromNodeListListener listener)
{
final DatabaseReference databaseRef = firebaseInstance.rootRef.child(database_reference);
if (recursiveIterator < nodesList.size())
{
Query query = databaseRef.child(nodesList.get(recursiveIterator));
query.addListenerForSingleValueEvent(new ValueEventListener()
{
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot)
{
for (DataSnapshot snap : dataSnapshot.getChildren())
{
firebaseIterator++;
//Do you job with the current key
String key = snap.getKey();
keysList.add(key);
// if got the last key from key and iterated the last node
if (firebaseIterator == dataSnapshot.getChildrenCount() && recursiveIterator == nodeList.size() -1)
{
firebaseIterator = 0;// reset the iterators
recursiveIterator = 0;
return;
}
// if got the last key from current node
else if (firebaseIterator == dataSnapshot.getChildrenCount() )
{
recursiveIterator++; // increase the recursive iterator because we looped through all the keys under the current node
firebaseIterator = 0; // reset the recursiveIterator because we gonna need to loop again with default value
getKeysFromNodeList(listener); // call the same method
}
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError)
{
Log.e(TAG, "Firebase query failed " + databaseError);
return;
}
});
}
}
And when using the method,first create a listener and override the onFinished() and then call the method.
listener = new getKeysFromNodeListListener()
{
#Override
public void onFinished()
{
// Do whatever you gonna do after you got all the keys from all nodes.
}
};
getKeysFromNodeList(listener);
There's an example provided in NetworkBoundResource, but when I tried it out, if database returns a result, it does not fetch from network anymore. What I want is display data from database and trigger network at the same time and eventually replace data in database after network completes. Example codes will be much appreciated.
I would use room database to save your items in a table. Then you use Paging list to observe to that table. The first time you observe to that table, also do a request to network. When you receive the response delete all items from the table and insert the new ones (all this in a repository class, and in a background thread). This last step will update your paging list automatically as your paging list is observing to that room table.
Guess this article could be helpful:
https://proandroiddev.com/the-missing-google-sample-of-android-architecture-components-guide-c7d6e7306b8f
and the GitHub repo for this article:
https://github.com/PhilippeBoisney/GithubArchitectureComponents
The heart of this solution is UserRepository:
public LiveData<User> getUser(String userLogin) {
refreshUser(userLogin); // try to refresh data if possible from Github Api
return userDao.load(userLogin); // return a LiveData directly from the database.
}
// ---
private void refreshUser(final String userLogin) {
executor.execute(() -> {
// Check if user was fetched recently
boolean userExists = (userDao.hasUser(userLogin, getMaxRefreshTime(new Date())) != null);
// If user have to be updated
if (!userExists) {
webservice.getUser(userLogin).enqueue(new Callback<User>() {
#Override
public void onResponse(Call<User> call, Response<User> response) {
Toast.makeText(App.context, "Data refreshed from network !", Toast.LENGTH_LONG).show();
executor.execute(() -> {
User user = response.body();
user.setLastRefresh(new Date());
userDao.save(user);
});
}
#Override
public void onFailure(Call<User> call, Throwable t) { }
});
}
});
}
I have a firebase database from which I save and retrieve data from, to and from. I know how datasnapshot works inside an addValueEventListener. The problem is that this is only called or triggered when the firebase database detects change in its data. I only want to access data and read it to be able to store it in an arraylist or the same thing.
I have a code like this:
public void foo(){
DatabaseReference x= FirebaseDatabase.getInstance().getReference().child("x");
reservations.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
String userID = client.getId();
for(DataSnapshot snap : dataSnapshot.getChildren()){
if(snap.child("someId").equals(someId)) number++;
if(snap.child("userID").getValue().equals(client.getId())){
isAlreadyReserved = true; // if user has already reserved the item
alreadyReserved();
break;
}
Log.e("isAlreadyReserved: ", isAlreadyReserved+"");
numberOfReservations++;
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
if(isAlreadyReserved) {
alreadyReserved();
}
else if(number == numberOfCopies){
// material is no longer available
OtherActivity.showMaterialUnavailable();
}
else{
Reservation reservation = new Reservation();
reservation.setBookId(this.bookId);
reservation.setResID((numberOfReservations+1)+"");
reservation.setUserID(client.getId());
String key = reservations.push().getKey();
reservations.child(key).setValue(reservation);
Log.e("Reservations: ", "reserve successful");
AlertDialog.Builder builder = new AlertDialog.Builder(this.context);
builder.setTitle(R.string.reservationSuccess_title)
.setMessage(R.string.reservationSuccess_body);
AlertDialog dialog = builder.create();
dialog.show();
}
}
You can see that inside onDataChange I only count materials and set some flags, which I can supposedly do outside the ValueEventListener.
But I notice that this is faulty because onDataChange is called only when writing to the Firebase database occurs. Which should not be the case.
What can I do to loop through the values inside the DatabaseReference x without calling onDataChange, or without using DataSnapshot?
You cannot loop inside a DatabaseReference without using a listener. When we are talking about Firebase, we are talking only about listeners. So in order to get those values, you need to use a listener and than get the data out from the dataSnapshot.
What i think your problem is in your case, is that onDataChange method is called in an asynchronously way. This means that everything you are doing outsite this method is actually executed before onDataChange method has been called. So in order to understand what is actually going on, please see this post and this post. Reading this posts, will teach you how to query data in Firebase and how to retrieve data from Firebase asynchronously.
Hope it helps.
In order to get the values of DatabaseReference x, you should use addListenerForSingleValueEvent
x.addListenerForSingleValueEvent(new ValueEventListener()
{
#Override
public void onDataChange(DataSnapshot dataSnapshot)
{
//do something
}
#Override
public void onCancelled(DatabaseError databaseError)
{
//do something
}
});
as mentioned in the firebase documentation:
public void addListenerForSingleValueEvent (ValueEventListener
listener)
Add a listener for a single change in the
data at this location. This listener will be triggered once with the
value of the data at the location.
My goal
I want to check if the server's token is still valid, let's say I know that information just by calling this getter : preferenceHelper.isTokenValid(). Then, if the token is invalid, calling a request to get a new token and updating the token locally, THEN, proceed with the next request to post the point to the server. That's because I need a valid token in order to make any further server request.
Let say I have those two server request that returns Observable:
This request is meant to get the server token, then upon reception, updating it.
Observable<Response<EntityToken>> updateServerToken = retrofitApi.authenticate(username,password);
This request is meant to post the current location to the server, then if it succeed, return the saved point
Observable<Response<EntityPoint>> updateServerToken = retrofitApi.postPoint(point);
Issues i'm facing currently:
Both observable that needs to be merged are from different type
Executing the token update request only if it needs to
Waiting for the token update request to complete before executing the request to post points
How should I write my RxJava Observable to satisfy all those condition?
First, I would create a method that checks if the entityToken is valid or not. If valid, use Observable.just() but you have to create an instance of Response somehow. If invalid, then call the server using the API in your requirement retrofitApi.authenticate(). Either path is taken, the method getTokenObservable() emits Observable<Response<EntityToken>>.
public Observable<Response<EntityToken>> getTokenObservable(EntityToken entityToken, String username, String password) {
boolean isTokenValid = preferenceHelper.isTokenValid(entityToken);
if (isTokenValid) {
//my assumption that you have something like this
Response<EntityToken> responseToken = new Response<EntityToken>();
responseToken.setEntityToken(entityToken);
return Observable.just(new Response<EntityToken>(entityToken.class));
} else {
Observable<Response<EntityToken>> updateServerToken = retrofitApi.authenticate(username, password);
return updateServerToken;
}
}
and then when calling it, use flatMap() which take emisssions of Observable<Response<EntityToken>> and returns emissions of Observable<Response<EntityPoint>>. Subscribe and proceed as normal.
Observable<Response<EntityToken>> updatePointObservable = getTokenObservable(entityToken, username, password);
updatePointObservable
.flatMap(new Func1<Response<EntityToken>, Observable<Response<EntityPoint>>>() {
#Override
public Observable<Response<EntityPoint>> call(Response<EntityToken> responseToken) {
EntityToken entityToken = responseToken.getEntityToken(); //my assumption
saveTokenLocally(entityToken); //this is where you save your token locally, change to the right method that you have
Observable<Response<EntityPoint>> updateServerTokenObservable = retrofitApi.postPoint(point, entityToken); //pass your entityToken to the call?
return updateServerTokenObservable;
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Response<EntityPoint>>() {
#Override
public void onCompleted() {
//your own logic
}
#Override
public void onError(Throwable e) {
//your own logic
}
#Override
public void onNext(Response<EntityPoint> entityPoint) {
//your own logic
}
});
As there is a dependency between the three calls, merge does not make any sense. instead, use flatMap:
Observable<Response<EntityPoint>> response =
retrofitApi.isTokenValid()
.flatMap(isValid ->
isValid
? Observable.just("")
: retrofitApi.authenticate(username,password)
.doOnNext(token -> doSomethingWithTheToken(token)
)
.flatMap(dummy -> retrofitApi.postPoint(point));
So I went through the documentation about how to retrieve datain Firebase: https://firebase.google.com/docs/database/android/retrieve-data
In my firebase app I am retrieving the values using the addListenerForSingleValueEvent.
Those values retrieved(here userIdKey) in the onDataChange method are used to make updates to some other places in the database.
What I have observed is that sometimes the updates are made everywhere except
at 123 (refer the code below).
DatabaseReference channelFollowersRef = mDatabase.child("followers").child(mKey);
channelFollowersRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for(DataSnapshot childSnapshot:dataSnapshot.getChildren()){
String userIdKey=childSnapshot.getKey();
/*123*/ delChannelMap.put("/user-subscriptions/" + userIdKey + "//" + mChannelKey,null);
}
delChannelMap.put("/channels/" + mChannelKey, null);
delChannelMap.put("/user-channels/" + getUid() + "/" + mChannelKey, null);
delChannelMap.put("/channels-notices/" + mChannelKey, null);
delChannelMap.put("/channels-subChannels/" + mChannelKey, null);
delChannelMap.put("/channels-subChannels-comments/" + mChannelKey, null);
delChannelMap.put("/channel-followers/" + mChannelKey, null);
mDatabase.updateChildren(delChannelMap, new DatabaseReference.CompletionListener() {
#Override
public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
Toast.makeText(ChannelDetailActivity.this, "Channel is now deleted.", Toast.LENGTH_SHORT).show();
}
});
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
How to solve this issue.
Also can someone elabrate on how onDataChange works , does it get called when all the values at the node are retrieved or 'some' of the data is retrieved ?
Should one use AsyncTask in such a scenario when retrieving data.
onDataChange gives you all the underlying child nodes at a single time whenever there is a change in the child nodes.
Firebase when retrieve data it uses a different thread to perform its task so no need of using AsyncTask.
the problem in your code maybe due to this String userIdKey=childSnapshot.getKey();...where the childSnapshot.getKey(); is not firing the data u want.. You should check in LOGCAT if data is retrieved correctly.
If it is not clear yet please add Json Data of your database so that it could be easier to understand what is happening.
Your code seems to delete fanned out data from many locations.
When you perform a multi-location update() such as this, the security rules for all nodes are checked before any of the changes are made. So (unless there is a bug in the way Firebase enforces these specific security rules), the disconnect can't be caused by security rules.
If your other nodes are updated, but /user-subscriptions/" + userIdKey + "//" + mChannelKey is not deleted, then you are likely not getting any children in dataSnapshot (and thus not passing them in delChannelMap). You'll want to run through the code in a debugger to verify that.
Whenever troubleshooting issues such as this, be sure to handle onCancelled(). It is the easiest way to get an indication of why an operation failed. Although I don't think it will execute here (after all, the write operation seems to succeed), I recommend you attach a completion callback to setValue:
ref.setValue("My new value", new DatabaseReference.CompletionListener() {
public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
throw databaseError.toException();
}
});
Throwing an exception like this ensures that it will be very difficult to overlook such an error next time.