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
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 want to store locally the data I am reading from the cloud.
To achieve this I am using a global variable(quizzes) to hold all the data.
For this, when I am building my Quiz objects, I need to make sure that before I am creating them, the relevant data has been already downloaded from the cloud. Since when reading data from firestore, it happens asynchronously.
I didn't enforced this (waiting for the read to finish) before -I just used onSuccess listeners, and I encountered synchronization problem because the reading tasks weren't finished before I created my Quiz objects with the data from the cloud.
I fixed this with a very primitive way of "busy waiting" until the read from the cloud is complete. I know this is very stupid, a very bad practice, and making the application to be super slow, and I am sure there is a better way to fix this.
private void downloadQuizzesFromCloud(){
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
CollectionReference quizzesRefrence = db.collection("users").document(user_id).collection("quizzes");
Task<QuerySnapshot> task = quizzesRefrence.get();
while(task.isComplete() == false){
System.out.println("busy wait");
}
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
}
I looked online in the documentation of firestore and firebase and didn't find anything that I could use. (tried for example to use the "wait" method) but that didn't help.
What else can I do to solve this synchronization problem?
I didn't understand if you tried this solution, but I think this is the better and the easier: add an onCompleteListener to the Task object returned from the get() method, the if the task is succesfull, you can do all your stuff, like this:
private void downloadQuizzesFromCloud(){
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
CollectionReference quizzesRefrence = db.collection("users").document(user_id).collection("quizzes");
quizzesRefrence.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccesful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
}
});
}
}
In this way, you'll do all you have to do (here the for loop) as soon as the data is downloaded
You can make your own callback. For this, make an interface
public interface FireStoreResults {
public void onResultGet();
}
now send this call back when you get results
public void readData(final FireStoreResults){
db.collection("users").document(user_id).collection("quizzes")
.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
results.onResultGet();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
results.onResultGet();
}
});
}
Now in your activity or fragment
new YourResultGetClass().readData(new FireStoreResults(){
#Override
public void onResultGet() {
new YourResultGetClass().getQuizzes(); //this is your list of quizzes
//do whatever you want with it
}
Hope this makes sense!
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!
I'm using Firebase Cloud Firestore with Firestore UI RecyclerView to display items on MainActivity. Everything works fine except that, when I uninstall and re-install the app, query does not fetch previously added items and only an empty list appears. When I add a new item to Firestore after re-install, only that item is fetched and still no previous data. However, I can see both previously added data and the new item on Firebase Console.
Has anyone experienced a similar issue or any idea what can cause this?
My function that sets up the RecyclerView is as follows. I call this function and then call adapter.startListening() in onStart and adapter.stopListening() in onStop.
private void setupRecyclerView() {
if(shouldStartSignIn()) return;
if(!PermissionUtils.requestPermission(this, RC_STORAGE_PERMISSION, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Log.e(TAG, "Permission not granted, don't continue");
return;
}
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
pagesRecyclerView.setLayoutManager(layoutManager);
Query query = FirebaseFirestore.getInstance().collection(Page.COLLECTION).whereEqualTo(Page.FIELD_USER_ID, FirebaseAuth.getInstance().getCurrentUser().getUid()).orderBy(Page.FIELD_TIMESTAMP).limit(50);
FirestoreRecyclerOptions<Page> options = new FirestoreRecyclerOptions.Builder<Page>().setQuery(query, Page.class).build();
adapter = new PagesAdapter(options, this);
pagesRecyclerView.setAdapter(adapter);
}
You have initialized yourquery but the query is not attached to any listener which actually pulls the data. So your adapter is empty. Basically you need to do something like this:
query.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
// Handle error
//...
return;
}
// Convert query snapshot to a list of chats
List<Chat> chats = snapshot.toObjects(Chat.class);
// Update UI
// ...
}
});
You can read more about it here: https://github.com/firebase/FirebaseUI-Android/tree/master/firestore#querying
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);
}
});
}