Problem: My list items display in the wrong order. This happens when I close the fragment and re-open it. It then displays all the "sent messages" first, and then the received messages after. However, when I'm in writing the messages, they appear in the correct order. It's only when I close the fragment/activity and re-open it that the order has changed.
I call the getMessages method in my on-create method for opening the fragment containing the view.
What I've tried:
Using the Firestore orderby method (both with String and TimeStamp)
Using the simpler Firestore Snapshot Listener
Question:
How do I best use the Firestore Snapshot Listener with a RecyclerView and maintain the order of the items correctly?
Here is my main "getMessages" method:
public void getLiveChatMessages(final ArrayList<ChatConversationMessage> messageArrayList, final ChatConversationAdapter adapter, final String matchClicked) {
final String userID = onboardingFirebaseUser.returnCurrentUserId();
final CollectionReference messagesCollectionRef = db.collection("users")
.document(userID)
.collection("matches")
.document(matchClicked)
.collection("messages");
messagesCollectionRef
.orderBy("TimeStamp", Query.Direction.DESCENDING)
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "listen:error", e);
return;
}
for (QueryDocumentSnapshot doc : value) {
if (doc.get("Message") != null) {
if (doc.get("Message") != null && doc.get("From user with ID").equals(userID)) {
String message = doc.getString("Message");
messageArrayList.add(new ChatConversationMessage(message));
adapter.notifyDataSetChanged(); //Ensures messages are visible immediately
} else if (doc.get("Message") != null) {
final String message = doc.getString("Message");
DocumentReference matchRef = db.collection("users")
.document(userID)
.collection("matches")
.document(matchClicked);
matchRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document.exists()) {
imageReference = storageReference.child(document.getString("profileImg"));
messageArrayList.add(new ChatConversationMessage(message, imageReference));
adapter.notifyDataSetChanged(); //Ensures messages are visible immediately
} else {
Log.d(TAG, "No such document");
}
} else {
Log.d(TAG, "get failed with ", task.getException());
}
}
});
}
}
}
}
});}}
After some time I've found the problem.
I was calling a the get-method on a new reference within the snapshot listener. When you do this, it impacts the order of the items in your ArrayList.
To solve it, ensure that all the items you need from Firestore to create your ArrayList is stored in the same location as fields on each document (and not in two different locations). That way, you don't need to use a separate get-method on a new reference within a snapshot listener. This also keeps client-side code cleaner. For future Googlers, here is how I restructured my method:
messagesCollectionRef
.orderBy("TimeStamp", Query.Direction.ASCENDING)
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot snapshots,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "listen:error", e);
return;
}
for (DocumentChange dc : snapshots.getDocumentChanges()) {
switch (dc.getType()) {
case ADDED:
Log.d(TAG, "New message added" + dc.getDocument().getData());
if (dc.getDocument().get("Message") != null && dc.getDocument().get("From user with ID").equals(userID)) {
String message = dc.getDocument().getString("Message");
messageArrayList.add(new ChatConversationMessage(CURRENTUSER, message, null));
adapter.notifyDataSetChanged();
}
if (dc.getDocument().get("Message") != null && dc.getDocument().get("From user with ID").equals(matchClicked)) {
String message = dc.getDocument().getString("Message");
imageReference = storageReference.child(dc.getDocument().getString("profileImg"));
messageArrayList.add(new ChatConversationMessage(OTHERUSER, message, imageReference));
adapter.notifyDataSetChanged();
}
break;
case MODIFIED:
break;
case REMOVED:
break;
}
}
}
});
As you can see, I've now stored the imageReference String within each message doc in Firestore, and it can be retrieved in the same way I retrieve all the other data I need to make an addition to my ArrayList. The major code change you need to do is where you write your data to the cloud (ie. write/set Firestore docs). That's where you'll need to make sure that everything is added as field values to your doc, so you don't need to get it in separate locations. Good luck!
Related
I made an application that users can earn points by taking quizzes. I have a leader board too.
my primary database is cloud firestore. but I need the leaderboard to be more real-time like it needs to update every time when a user earns points without refreshing or closing the fragment.
So I need to connect firebase firestore to real-time databases, (if I change the firestore data(like coins for the specified user or any), it needs to change the real-time data too)
I made codes but it didn't work well. I have attached the code here.
private void LoadFirestore() {
firebaseFirestore.collection("Users")
.document(FirebaseAuth.getInstance().getUid())
.get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
user = documentSnapshot.toObject(User.class);
totalCoins.setText(String.valueOf(user.getCoins()));
}
});
}
private void uploadToRealtime() {
HashMap<String, Object> map = new HashMap<>();
map.put("coins", totalCoins);
firebaseDatabase.getReference().child("Users").child(FirebaseAuth.getInstance().getUid())
.updateChildren(map);
}
}
You can use a onSnapshotListener to get the fata directly from firestore in realtime. Here is a basic example for that:
final DocumentReference docRef = db.collection("cities").document("SF");
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, "Current data: " + snapshot.getData());
} else {
Log.d(TAG, "Current data: null");
}
}
});
You can check more about it here.
Creating a sync to the RealtimeDatabase just for the realtime feature wouldn't make any sense here and would give you more costs in your Firebase project.
I have one Fragment that updates a field in Firestore and then after the update the next Fragment gets opened. There the updated data should be displayed in two TextViews but currently the method I used to retrieve the date from Firestore is loading the old data, not the from the previous Fragment updated data.
It seems that the new Fragment gets opened too fast so it still reads the old data from Firestore, how can I achieve that it retrieves the updated data?
I already tried to call the method in Fragment 2 in the onViewCreated and onStart method but also there it gets the old data:
Here is my first Fragment that updates the fields in Firestore before opening the next Fragment: ( I call the method below in the onClickmethod)
...
gamesRef.document(gameId).update(currentUserUid, pointsPlayer1Str)
.addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
((GameActivity) getActivity()).setViewPager(7);
}
});
...
The I try to retrieve the updated data in the second Fragment, in the onCreateView:
...
gameRef.document(gameId).get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document != null) {
pointsPlayer1Str = document.getString(currentUserUid);
pointsPlayer2Str = document.getString(uidPlayer2);
textViewPointsCurrentUser.setText(pointsPlayer1Str);
textViewPointsOpponent.setText(pointsPlayer2Str);
}
}
}
});
...
Where should I call the method from the second fragment to get the updated data?
Any help is much appreciated!
You can attach a real-time listener to your data instead with the following:
val docRef = gameRef.document(gameId)
docRef.addSnapshotListener { snapshot, e ->
if (e != null) {
Log.w(TAG, "Listen failed.", e)
return#addSnapshotListener
}
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, "Current data: ${snapshot.data}")
} else {
Log.d(TAG, "Current data: null")
}
}
I Have a chat list(RecyclerView) which I retrieve from Firestore which I sort with respect to the timestamp. The problem which occurs is The list get updated only when the activity is created Else it sits idle.
I have tried Running fetching code in the Onresume as well.But it just creates another set of same items.
firebaseFirestore = FirebaseFirestore.getInstance();
Query secondquery = firebaseFirestore.collection("Users").document(currentUser).collection("Chat").orderBy("timestamp",Query.Direction.DESCENDING);
secondquery.addSnapshotListener((MainActivity.this), new EventListener<QuerySnapshot>() {
#Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
for (DocumentChange doc : documentSnapshots.getDocumentChanges()) {
if (doc.getType() == DocumentChange.Type.ADDED) {
String UserId=doc.getDocument().getId();
ChatList chatList = doc.getDocument().toObject(ChatList.class).withId(UserId);
chatLists.add(chatList);
chatListAdapter.notifyDataSetChanged();
}
}
}
});
I expect it to get updated as soon as the new document is added. But it remains idle until I reopen the activity
I am have the follow code:
public synchronized void next(final RoomListQueryResultHandler handler) {
this.setLoading(true);
roomList = new ArrayList<Room>();
this.database.child("members").child(this.mUser.getUid()).child("rooms")
.limitToFirst(this.mLimit)
.startAt(this.currentPage * this.mLimit)
.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
RoomListQuery.this.setLoading(false);
//mListAdapter.setLoading(false);
if (!dataSnapshot.hasChildren()) {
RoomListQuery.this.currentPage--;
}
for (DataSnapshot ds : dataSnapshot.getChildren()) {
Room room = ds.getValue(Room.class);
//roomList.add(Room.upsert(room));
Room.getRoom(room.getId(), new Room.RoomGetHandler() {
#Override
public void onResult(Room room, customException e) {
if (e != null) {
// Error!
e.printStackTrace();
return;
}
roomList.add(room);
}
});
handler.onResult(roomList, (customException) null);
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
handler.onResult((List) null, new customException(databaseError.toString()));
}
});
}
}
If they are see, I have two Handlers, at first I call a list of "rooms" from Firebase, and then for each one I get the detail in other query.
The problem is that the response is a empty list, since the function not wait for all query details to be executed for the rooms, so the variable roomList always returns empty.
Any idea what I can implement, or what other methodology to use to solve it?
Thank you very much!
Greetings.
Depending on how your application is structured, you might want to change the database design so that there is no need to perform an additional Firebase query for each room retrieved from the first query.
//mListAdapter.setLoading(false);
If you're creating a list view where each row is from the /members/<user_id>/rooms Firebase node, what are the minimum room attributes necessary to display that list? If it's just a few things like room name, photo url, owner, room_id, etc you might be better off duplicating those from the original source. Then clicking one of those rows can trigger the original additional Firebase query you had as part of Room.getRoom(room.getId(), new Room.RoomGetHandler() { ... });, to navigate to a new screen / display a modal with the full room details once retrieved.
Update
To address your comment about requiring the extra data, in that case, as part of the Room class I would include an extra boolean value _loadedDetails set initially to false. So that for rendering a room within the list, when _loadedDetails is currently false just display a loading spinner. That way you can still perform those additional queries and when completed, update the appropriate Room object within roomList based on the index. Something like this:
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
RoomListQuery.this.setLoading(false);
//mListAdapter.setLoading(false);
if (!dataSnapshot.hasChildren()) {
RoomListQuery.this.currentPage--;
}
int i = 0;
for (DataSnapshot ds : dataSnapshot.getChildren()) {
Room room = ds.getValue(Room.class);
roomList.add(room); // here instead
updateRoom(room, i);
i++;
}
handler.onResult(roomList, (customException) null);
}
...
// outside of the ValueEventListener
public void updateRoom(room, index) {
Room.getRoom(room.getId(), new Room.RoomGetHandler() {
#Override
public void onResult(Room room, customException e) {
if (e != null) {
// Error!
e.printStackTrace();
return;
}
room._loadedDetails = true; // make that publicly accessible boolean, or include a setter method instead
roomList.set(index, room);
}
});
}
Following up on this grate answer I use the ValueEventListener to listen for only new items with this query:
Query query = ref.child(USER)
.child(mFireBaseAuth.getCurrentUser().getUid())
.child(TOYS)
.orderByKey().startAt(-KXN04BDYdEG0aii9mMY);
query.addValueEventListener(mvalueEventListener);
The -KXN04BDYdEG0aii9mMY in the code sample above is the latest item i already have and I want to get only push keys after that time stamp. With this code I also get the -KXN04BDYdEG0aii9mMY again and that is not needed because I already have it right.
How should I solve this? Maybe add a millisecond to this Firebase push key -KXN04BDYdEG0aii9mMY?
Under the TOYS key is a list of Firebase Puch keys
There is no way to specify an exclusive anchor with Firebase Database queries, so you will always get the key that you specify for startAt() (and also if you specify it endAt()). So you'll have to skip the first/last item in your client-side code.
I got it working just like I was asking for in the question. Here is the code since it´s the answer for me, and for anyone's delight.
This was a tricky one I have to both filter away the key that I specify for startAt() and at the same time in the ´onDataChange()´ handle a second call because of TimeStamp correcting from Firebase, when doing ´...ServerValue.TIMESTAMP);´
// When startAt() get defined this will make sure that we dont save the
// startAt key item again.
// Best would be if the startAfter("pushKey") existed but it does not.
String skipStartAtKey = "";
/**
* Typically when user sign in or app start
*/
public void userSignedIn() {
Log.i(TAG, "userSignedIn");
// start listening
activateFirebaseListeners();
}
/**
* When user sign in this listeners are started
*/
private void activateFirebaseListeners() {
// [START Listen for history Address changes ]
/**
* The ChildEventListener.
* This is only used when app first install or user wipe data
*/
ChildEventListener userHistoryAddressChildEventListener = new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot snapshot, String prevKey) {
userHistoryAddressChildEvent(snapshot, prevKey);
}
#Override
public void onChildChanged(DataSnapshot snapshot, String prevKey) {
userHistoryAddressChildEvent(snapshot, prevKey);
}
#Override
public void onCancelled(DatabaseError error) {
// TODO dont forget to remove reset listeners
}
#Override
public void onChildRemoved(DataSnapshot snapshot) {
}
#Override
public void onChildMoved(DataSnapshot snapshot, String prevKey) {
}
};
// If client don´t have any history start the ChildEventListener.
// Typically this is the first time app starts or user have cleared data.
if (ToyManager.getInstance().getHistoryItems(mFireBaseAuth.getCurrentUser().getUid()).size() == 0) {
Log.i(TAG, "HistoryItems SIZE = 0 starting ChildEventListener");
// Local cache of address history is empty get all address history for this user and start listen for new items
final Query query = ref.child(USER_HISTORY).child(mFireBaseAuth.getCurrentUser()
.getUid())
.child(TOYS);
mChildListenerMap.put(query.getRef(), userHistoryAddressChildEventListener);
query.addChildEventListener(userHistoryAddressChildEventListener);
} else {
// If client have history then start the ValueEventListener.
// Typically this is not the first time app starts.
startListenForUserAddressHistory();
}
// [END Listen for history Address changes ]
}
private void userHistoryAddressChildEvent(DataSnapshot snapshot, String prevKey) {
// get history for current user
UserHistory.AddressHistory addressHistory = snapshot.getValue(UserHistory.AddressHistory.class);
ToyManager.getInstance().addHistory(
mFireBaseAuth.getCurrentUser().getUid(),
addressHistory.getAddressId(),
addressHistory.getTime(),
addressHistory.getToy(),
addressHistory.getPushKey());
}
/**
* Start listen for nye entries based on that server have entries<br>
* and client have entries, typically this is not the first time App starts.
* This uses a ValueEventListener.
*/
private void startListenForUserAddressHistory() {
// Local history is not empty so we must go get only new items
final ValueEventListener listener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// get history for current user
if (dataSnapshot.getChildrenCount() != 0) {
for (DataSnapshot child : dataSnapshot.getChildren()) {
UserHistory.AddressHistory addressHistory = child.getValue(UserHistory.AddressHistory.class);
if (dataSnapshot.getChildrenCount() == 1) {
// child count is one so this can be a SERVERTIME correctness call.
// skip the skipStartAtKey if key and time is the same.
if (addressHistory.getPushKey().equals(skipStartAtKey)) {
// get historyItem
HistoryItem historyItem = ToyManager.getInstance().getHistoryItem(mFireBaseAuth.getCurrentUser().getUid().concat(addressHistory.getPushKey()));
// compare time
if (historyItem.getTime().toString().equals(addressHistory.getTime().toString())) {
Log.i(TAG, "time is the same exiting with return");
return;
} else
Log.i(TAG, "time is different");
} else {
Log.i(TAG, "PushKey not same as skipStartAtKey");
}
} else if (dataSnapshot.getChildrenCount() > 1) {
// two children or more so lets dump the skipStartAtKey and save the rest
Log.i(TAG, "TESTING getChildrenCount > 1" + " skipStartAtKey " + skipStartAtKey + " time " + addressHistory.getTime());
if (addressHistory.getPushKey().equals(skipStartAtKey)) {
Log.i(TAG, "PushKey same as skipStartAtKey");
continue;
}
}
ToyManager.getInstance().addHistory(
mFireBaseAuth.getCurrentUser().getUid(),
addressHistory.getAddressId(),
addressHistory.getTime(),
addressHistory.getToy(),
addressHistory.getPushKey());
}
ValueEventListener v = mValueListenerMap.get(dataSnapshot.getRef());
Log.i(TAG, "removing ValueEventListener for");
dataSnapshot.getRef().removeEventListener(v);
startListenForUserAddressHistory();
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
Log.w(TAG, "onCancelled" + databaseError.toException());
// TODO dont forget to remove reset listeners
}
};
// get the latest history item
HistoryItem historyItem = ToyManager.getInstance().orderHistoryByDateAndGetNewest(mFireBaseAuth.getCurrentUser().getUid());
final Query query = ref.child(USER_HISTORY)
.child(mFireBaseAuth.getCurrentUser().getUid())
.child(TOYS)
.orderByKey().startAt(historyItem.getPushKey()); // start listen on the latest history
mValueListenerMap.put(query.getRef(), listener);
skipStartAtKey = historyItem.getPushKey();
query.addValueEventListener(listener);
}
/**
* App is closing or User has signed out
* Called from both onClose() and userSignedOut()
*/
private void closeFirebase() {
userLogger.log(USER_SIGNED_OUT);
// Close ChildEventListener(s)
for (Map.Entry<DatabaseReference, ChildEventListener> entry : mChildListenerMap.entrySet()) {
DatabaseReference ref = entry.getKey();
ChildEventListener listener = entry.getValue();
if (listener != null && ref != null)
ref.removeEventListener(listener);
}
// Close ValueEventListener(s)
for (Map.Entry<DatabaseReference, ValueEventListener> entry : mValueListenerMap.entrySet()) {
DatabaseReference ref = entry.getKey();
ValueEventListener listener = entry.getValue();
if (listener != null && ref != null)
ref.removeEventListener(listener);
}
// Close
if (userValueEventListener != null)
ref.child(USER).child(mFireBaseAuth.getCurrentUser().getUid())
.removeEventListener(userValueEventListener);
if (userAuthListener != null)
mFireBaseAuth.removeAuthStateListener(userAuthListener);
userLogger = null;
}