I fetch data that is constantly changing from Firebase realtime database.
Then I check if an item exists in an ArrayList, I don't add it, but if it does, I add and notify that the item was inserted. But, since the database is constantly changing, the entire list is "blinking". How can I add remove items inside the database listener, without making it blink? i.e only add/remove the specific item that is changing. This is the code now:
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for(DataSnapshot datas: dataSnapshot.getChildren()){
if (x<token){
if (!myDataset.contains(datas.getKey().toString())){
int position = myDataset.indexOf(datas.getKey().toString());
myDataset.add(datas.getKey().toString());
mAdapter.notifyItemInserted(myDataset.size() - 1);
}
} else {
myDataset.remove(datas.getKey().toString());
int position = myDataset.indexOf(datas.getKey().toString());
mAdapter.notifyItemRemoved(position);
}
}
Your current code notifies the adapter of each individual item in the list, whenever there are any changes. The smallest possible change that will reduce the blinking is to move the notifying of the adapter outside of your loop:
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
bool isChanged = false;
for(DataSnapshot datas: dataSnapshot.getChildren()){
if (x<token){
if (!myDataset.contains(datas.getKey().toString())){
int position = myDataset.indexOf(datas.getKey().toString());
myDataset.add(datas.getKey().toString());
isChanged = true;
}
} else {
myDataset.remove(datas.getKey().toString());
int position = myDataset.indexOf(datas.getKey().toString());
isChanged = true;
}
if (isChanged) {
mAdapter.notifyDataSetChanged()
}
}
}
Now for any update to the values, you will only notify the adapter (and hence force it to repaint) only once.
I'd recommend a different approach though. Since you are interested in additions and removal of child nodes, it is likely more efficient to use a ChildEventListener. This listener has callback methods for specific changes, such as onChildAdded, and onChildRemoved, that get called when a child is added or removed respectively. These callbacks get the precise information you need t update your myDataset.
You should still also use a ValueEventListener to detect when all changes have been received, and user that to notify the adapter of the changes, so that it rerenders the view. Since ValueEventListener is guaranteed to fire after the corresponding ChildEventListener callbacks, this sequence will work.
So:
Use a ChildEventListener to listen to more granular changes, to allow direct updates of the myDataset without having to loop and detect it yourself.
Use a ValueEventListener to detect when the child-level changes are done, and notify the adapter that it needs to repaint the view.
For a small example of this approach in production code, see the FirebaseArray class in the FirebaseUI library.
Related
I am designing a simple basic chat app using firebase real time database and i've designed everything well, however, i'm facing one sllight issue. My chats keep duplicating themselves on the inbox page (the page whrere the chats are laid out for a user to select which chat he wants to open and start talking).
I've attached an image of what i mean below.
Screenshot of the phone screen
The code i am using to get the chats and display them in the recycler view is given below. I have a directory called Conversations in my DB that saves a user's Id and under it, theres a child of each and every person he chats wit, under which is the last message and a seen boolean.
Database Structure
The code is given below
convoref = FirebaseDatabase.getInstance().getReference().child("Conversations").child(currentUid);
and then..
public void getConvoIds() {
convoref.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
for(DataSnapshot convo : dataSnapshot.getChildren()){
boolean isMessageSeen = false;
String lastMessage = "";
if(convo.child("seen").getValue() != null) {
isMessageSeen = (boolean) convo.child("seen").getValue();
}else{
Log.i("nolastseen", "location is null");
}
if(convo.child("lastMessage").getValue() != null) {
lastMessage = convo.child("lastMessage").getValue().toString();
}else{
Log.i("nolastMessage", "location is null");
}
Log.i ("the_convo_partner_key", convo.getKey());
Log.i ("lastseenmessage", lastMessage);
Log.i ("seenstate", String.valueOf(isMessageSeen));
FetchConvoInfo(convo.getKey(), isMessageSeen, lastMessage );
}
}
}
the fetch convo information functuion is below
public void FetchConvoInfo(final String key, final boolean isMessageSeen, final String lastMessage){
FirebaseDatabase.getInstance().getReference().child("Users").child(key).addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
boolean chatExists = false;
String username = "";
String thumbnail = "";
String chatPartner;
chatPartner = key;
if(dataSnapshot.child("username").exists()){
username = dataSnapshot.child("username").getValue().toString();
}
if(dataSnapshot.child("thumbnail").exists()){
thumbnail = dataSnapshot.child("thumbnail").getValue().toString();
}
ConvoClass obj= new ConvoClass(chatPartner, username, thumbnail, isMessageSeen, lastMessage);
resultConvos.add(obj);
mConvoAdapter.notifyDataSetChanged();
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
}
Any help would be greatly appreciated. i cant seem to figure out why the chat duplicates.
In your onDataChanged method, you are going through every child of the dataSnapshot. Each child of the data snapshot indicates a particular conversation of that particular currentUid guy. So when you are going through every child of the dataSnapshot you are adding all its children to the listview or recyclerview(I don't know what you are using. But you are adding it to the adapter). So you are adding the old data again and again whenever some new data must be added. Hence-duplicate data.
There are two common solutions.
The first is naive method. Do what you are doing right now. But while adding an item (chat, you will call it in your case, I think) to the adapter, check if it is already present in the container resultConvos. That will prevent you from adding duplicate chats. I am sure it is obvious to you also why this method is inefficient. You are unnecessarily having to go through every conversation of a person. It takes O(n) time for just adding one item.
The second method is the recommended method. Remove all the code of ValueEventListener. Instead use ChildEventListener. I don't know if you are aware of it. Check this.
ChildEventListener has mainly 4 methods instead of onDataChanged. Among that, what you require here is onChildAdded. Just like your onDataChanged, it has one argument- a data snapshot. But this data snapshot contains only the newly added child, whereas the data snapshot in onDataChanged contains the whole data of the conversations of that particular user (that means the whole list). So using the data snapshot provided by onChildAdded you can directly add only that chat to the adapter, which takes O(1) time.
For more about ChildEventListener, read that link I attached
Actually i want to get online users only not the whole list of users.
This is my database structure:
Users
- Uid
- online : true
i want to get list of users which are online without needing to get each one of the user and then sorting it by checking the datasnapshot.
Assuming that Users node is a direct child of the Firebase root, i recomand you using the following code:
DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
Query query = rootRef
.child("Users")
.child("Uid")
.orderByChild("online")
.equalTo(true);
query.addListenerForSingleValueEvent(/* ... */);
Edit1: If you don't have the uid of each user, please use the following code:
DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
DatabaseReference usersRef = rootRef.child("Users");
ValueEventListener eventListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for(DataSnapshot ds : dataSnapshot.getChildren()) {
String uid = ds.getKey();
Query query = usersRef.child(uid).orderByChild("online").equalTo(true);
query.addListenerForSingleValueEvent(/* ... */);
}
}
#Override
public void onCancelled(DatabaseError databaseError) {}
};
usersRef.addListenerForSingleValueEvent(eventListener);
Edit2: As you can see in the offical documentation, addListenerForSingleValueEvent adds 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.
When you write data and if you'd like to know when your data has been committed, you can add a OnCompleteListener which is called when the Task completes. Both setValue() and updateChildren() methods take an optional completion listener that is called when the write has been committed to the database. This is an example:
yourRef.setValue("I'm writing data", new Firebase.CompletionListener() {
#Override
public void onComplete(FirebaseError firebaseError, Firebase firebase) {
if (firebaseError != null) {
System.out.println("Data could not be saved. " + firebaseError.getMessage());
} else {
System.out.println("Data saved successfully.");
}
}
});
Regarding synchronizing, there is a common way to detect when Firebase is done synchronizing the initial data on a given location. This approach makes use of one of the Firebase event guarantees:
Value events are always triggered last and are guaranteed to contain updates from any other events which occurred before that snapshot was taken.
So if you have both a ValueEventListener and a ChildEventListener on a given location, the ValueEventListener.onDataChange() is guaranteed to be called after all the onChildAdded() calls have happened.
But one thing to keep in mind: Firebase doesn't just load data. It continuously synchronizes data from the server to all connected clients. As such, there is not really any moment where the data is completely retrieved. That's why you need to remove the listener as explained below.
Furthermore, note that you can remove the listener whenever you want. The best practice is to remove it accordingly to the life-cycle of your activity.
If you have added the listener in onStart you have to remove it in onStop.
If you have added the listener in onResume you have to remove it in onPause.
If you have added the listener in onCreate you have to remove it in onDestroy.
But remember onDestroy is not always called.
databaseReference.removeEventListener(valueEventListener);
You can filter your items by specifying the child and value like this:
ref.orderByChild("online").equalTo(true);
More info here: https://firebase.google.com/docs/database/android/lists-of-data
Use this code
ref.child('Uid')
.orderByChild('online')
.equalTo(true);
or use
FirebaseDatabase.getReference("Users").child("Uid").orderByChild("online").equalTo(true);
I have a listener set on the root of my Firebase Database -
database = FirebaseDatabase.getInstance();
rootDB = database.getReference();
I have a listener set up as follows -
groupListener = (new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Log.i("ValueEventListener", "Count :" + dataSnapshot.getChildrenCount());
.... more code...
I attach the listener as below -
rootDB.addValueEventListener(groupListener);
Then as I load a new activity, I want this to stop listening so I use -
rootDB.removeEventListener(groupListener);
It seems to keep listening, can anyone tell me what I've done wrong?
I know I can use a single event listener but I want it to keep monitoring while it's in this activity but not when in other activities.
Thanks!
three suggestions:
1. You may be adding listener more than once.
If a listener has been added multiple times to a data location, it is
called multiple times for each event, and you must detach it the same
number of times to remove it completely.
(from here)
2. The listener you are adding may not be the same one you are removing.
Here CASE 1 will remove the listener while CASE 2 will not:
//CASE 1:
ValueEventListener listener = new ValueEventListener() {....}
databaseRef.addValueEventListener(listener);
databaseRef.removeEventListener(listener);
//CASE 2:
public ValueEventListener listener(){
return new ValueEventListener() {....}
}
databaseRef.addValueEventListener(listener());
databaseRef.removeEventListener(listener());
3. As a workaround, you can set a boolean and change it's value when you want to stop/start listening:
groupListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if(isListening){
//...
}
}
Hope one of them helps..
This is maddening!
From what I can tell, the removeEventListener method is asynchronous and will eventually be removed. But if you are trying to quickly switch from one DatabaseReference to another (such as a $uid path), then you're going to have the duplicate calls. I'm not found a synchronous way to resolve this.
After hours of debugging and much of my live stolen from me, I extended the ChildEventListener interface, add the isListening functionality that #Ahmed mentions above. Now I see that while initially there are duplicate calls to onChildAdded, they do eventually subside.
GLWY!!
I have a ListView that each row contains several EditText and a save Button.
In the ShowOrder activty i'm getting the data from Firebase
public void getItemsOrderDetails(final String key){
orderDetailsRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
arrayLength = dataSnapshot.getChildrenCount();
String itemN="", discription="", qty="", pcsCtn="", ctnWt="", compileStatus="", palletN="", ttlCtn="";
if(!key.equals("costumerName") && !key.equals("dateOfDelivery") && !key.equals("dateOfOrder") && !key.equals("orderN") && !key.equals("remarks") && !key.equals("status")){
if(dataSnapshot.child(key).hasChild("itemNumber")){
itemN= dataSnapshot.child(key).child("itemNumber").getValue().toString();
}
...
... {downloading all data}
...
itemsClass= new ItemsClass(orderNumber, itemN, discription, pcsCtn, ttlCtn, ctnWt, qty, compileStatus, palletN );
itemsClassArrayList.add(itemsClass);
adapter.notifyDataSetChanged();
}
I'm showing all the data at the adapter , and problem is when I want to update the data onChildAdded is beeing triggered multiple times.
This is the update part in the Adapter:
private void updateData(ViewHolder vh, String orderN){
showOrder.itemsClassArrayList.clear();
updateStatus="idle";
ordersRef.child(orderNumber).child(itemN).child("palletNo").setValue(pallete);
ordersRef.child(orderNumber).child(itemN).child("pcsPizza").setValue(untCtn);
ordersRef.child(orderNumber).child(itemN).child("compileStatus").setValue(collected);
ordersRef.child(orderNumber).child(itemN).child("drink").setValue(ctnWt);
ordersRef.child(orderNumber).child(itemN).child("ttlWt").setValue(pltWt);
ordersRef.child(orderNumber).child(itemN).child("ttl").setValue(ttlCtn);
updateStatus= "update";
//showOrder.adapter.clear();
}
What I discovered is that if i'm updating only one child (Deleting all the others) the onChildAdded will be update only once.
So i don't understand how can i update all with out multiple Updating, if anyone as an idea.
Thank you
This might not be a complete solution to your issue, but a few things struck me right away:
You can (and should) load and store complete Objects in your database, so instead of reading and writing each child (e.g. of your ItemsClass object) separately, you should store your item like so:
ordersRef.child(orderNumber).child(itemN).setValue(myItem)
where myItem is an instance of ItemsClass that has been populated with data before.
In a similar way, loading data should be done like so (depending on where orderDetailsRef points to! see below):
public void onDataChange(DataSnapshot dataSnapshot) {
ItemsClass items = dataSnapshot.getValue(ItemsClass.class);
}
Also, you mention onChildAdded getting called multiple times, yet in your example you are using a ValueEventListener's onDataChange method.
Could you give a complete code sample? The one you posted is lacking relevant parts of your solution. And let me know how your code behaves when you load and save the way I suggested.
I'm currently developing an Android app that deals with storing book translations. I'm using Firebase to hold all the data online, and I have no trouble pulling data from it and storing them as Book objects. I have a master list of books in the main class that stores all the books from the Firebase database, however the list doesn't seem to update properly.
public static List<Book> books = new ArrayList<Book>();
protected void onCreate(Bundle savedInstanceState) {
myFirebaseRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
for (DataSnapshot bookSnapshhot : snapshot.getChildren()) {
Book book = bookSnapshhot.getValue(Book.class);
books.add(book);
System.out.println(book.getAuthor() + " - " + book.getTitle());
for (Chapter chapter : book.getChapters()) {
System.out.println(chapter.getTitle());
}
for (Genre genre : book.getGenres()) {
System.out.println(genre.getGenre());
}
System.out.println(books.size());
}
}
#Override public void onCancelled(FirebaseError error) { }
});
System.out.println(books.size());
}
In that body, the size of books is correct (right now it is only 1). However if I call the same print statement outside of that code body (the last line), it gives me 0. I don't know why, because it successfully adds it inside the onDataChange() method but it seems to not apply the changes on the outside.
I think that your problem is about the asyncronously of the ValueEventListener.
The onDataChange is called asynconously.
You have to use the books after the onDataChange is called.
The addValueEventListener() will fetch the data from Firebase asynchronously.
What your code depicts is that you are adding ValueEventListener and then printing System.out.println(books.size());. This means your logs gets displayed after you add ValueEventListener(which doesn't means your listener gets invoked). At this point your listener is not invoked yet and so isn't onDataChange(). Thus you won't ever get updated count outside the listener scope.