3
votes

I am making android chat app with firebase firestore database a I need infinite pagination with listeners for data changes (new massage, deleted massage...)

I found blog post written in kotlin and of corse searched firebase documentation and end up with this code:

    // firstTime variable shows if function is called from pagination or initially
    private void addMessagesEventListener(boolean firstTime) {
            // get collection
            CollectionReference messagesCollection =                         chatsCollection.document(chat.getId()).collection(Constants.FIREBASE_MESSAGES_PATH);
    // create query
    Query query = messagesCollection.orderBy("timestamp", Query.Direction.DESCENDING);
    // if NOT first time add startAt
    if (!firstTime) {
        query.startAt(startTimestamp);
    }
    //limit to 20 messages
    query.limit(20).get().addOnSuccessListener(queryDocumentSnapshots -> {
        if (!firstTime) {
            endTimestamp = startTimestamp;
        }
        startTimestamp = (long) queryDocumentSnapshots.getDocuments().get(queryDocumentSnapshots.size() - 1).get("timestamp");

        Query innerQuery = messagesCollection.orderBy("timestamp").startAt(startTimestamp);
        if(!firstTime) {
            innerQuery.endBefore(endTimestamp);
        }
        ListenerRegistration listener = innerQuery
                .addSnapshotListener((queryDocumentSnapshots1, e) -> {
                    if (e != null) {
                        Log.w(TAG, "listen:error", e);
                        return;
                    }

                    for (DocumentChange dc : queryDocumentSnapshots1.getDocumentChanges()) {
                        Message message = dc.getDocument().toObject(Message.class);
                        switch (dc.getType()) {
                            case ADDED:
                                // add new message to list
                                messageListAdapter.addMessage(message);
                                if (firstTime) {
                                    messagesList.smoothScrollToPosition(0);
                                }
                                break;
                            case REMOVED:
                                // remove message from list
                                messageListAdapter.removeMessage(message);
                                break;
                        }
                    }
                });
        listeners.add(listener);
    });
}

Now, code suppose to save listeners 1st for first 20 messages and new messages, 2nd for messages from 20-40 and so on, but, it is not working for some reason. Am I missing something?

Problem is that line startTimestamp = (long) queryDocumentSnapshots.getDocuments().get(queryDocumentSnapshots.size() - 1).get("timestamp"); gets always the same result. I tried even with documentSnapshot instead of timestamp, same result.

Thanks in advance.

2
You can't really combine paging with listening. You have to choose one or the other in order to maintain the sanity of your code.Doug Stevenson
@DougStevenson thank you for your answer. Can you explain to me why, when I add startAt with timestamp of 20th message, it returns 20 first messages like there is no condition?Sasa Grujic
This is a recommended way in which you can paginate queries by combining query cursors with the limit() method. I also recommend you take a look at this video for a better understanding.Alex Mamo
I have fixed the code. Just added query = query.startAt() and innerQuery = innerQuery.startAt()Sasa Grujic

2 Answers

1
votes

try this

 @Override
 public void onStart() {
        super.onStart();

        loadFirstQuery();
 }


    public void loadFirstQuery() {


        if (firebaseAuth.getCurrentUser() != null) {

            contentListDashboard.clear();

            String currentUserId = firebaseAuth.getCurrentUser().getUid();              

            // what we do when recycler reach bottom
            recyclerProfileDashboard.addOnScrollListener(new RecyclerView.OnScrollListener() {
               @Override
               public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                   super.onScrolled(recyclerView, dx, dy);

                   // horizontal
                   //Boolean reachBottom = !recyclerView.canScrollHorizontally(-1);

                   // for vertical recycler
                   Boolean reachBottom = !recyclerView.canScrollVertically(-1);


                   if (reachBottom) {
                       loadMorePost(); // do load more post
                   }
               }
           });

            // RETRIEVING FIRST Query
            Query firstQuery = firebaseFirestore
                    .collection("ProfileDashboard")
                    .document(currentUserId)
                    .collection("ProfileInfo")
                    .orderBy("timestamp", Query.Direction.DESCENDING)
                    .limit(20);    

            firstQuery.addSnapshotListener(new EventListener<QuerySnapshot>() {
                @Override
                public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {

                    if (!documentSnapshots.isEmpty()) {
                        // please add if doc not empty
                        if (isFirstPageFirstLoad) {
                            lastVisible = documentSnapshots.getDocuments().get(documentSnapshots.size() - 1); // array 0, 1, 2
                        }

                        for (DocumentChange doc : documentSnapshots.getDocumentChanges()) {

                            if (doc.getType() == DocumentChange.Type.ADDED) {

                                //String postId = doc.getDocument().getId();
                                contentProfileDashboard = doc.getDocument().toObject(ContentProfileDashboard.class);   


                                // if first page firest load true
                                if (isFirstPageFirstLoad) {
                                    contentListDashboard.add(contentProfileDashboard);
                                } else {
                                    contentListDashboard.add(0, contentProfileDashboard);
                                }


                                // fire the event
                                adapterProfileDashboard.notifyDataSetChanged();
                            }
                        }

                        isFirstPageFirstLoad = false;
                    }
                }
            });
        }
    }

   // Method to load more post
   public void loadMorePost() {

        if (firebaseAuth.getCurrentUser() != null) {

            String currentUserId = firebaseAuth.getCurrentUser().getUid();

            Query nextQuery = firebaseFirestore
                    .collection("ProfileDashboard")
                    .document(currentUserId)
                    .collection("ProfileInfo")
                    .orderBy("timestamp", Query.Direction.DESCENDING)
                    .startAfter(lastVisible)
                    .limit(20);

            nextQuery.addSnapshotListener(new EventListener<QuerySnapshot>() {
                @Override
                public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {

                    if (!documentSnapshots.isEmpty()) {

                        lastVisible = documentSnapshots.getDocuments().get(documentSnapshots.size() - 1);
                        for (DocumentChange doc : documentSnapshots.getDocumentChanges()) {

                            if (doc.getType() == DocumentChange.Type.ADDED) {

                                //String postId = doc.getDocument().getId();
                                // contentSeen = doc.getDocument().toObject(ContentProfile.class);
                                // contentList.add(contentSeen);

                                contentProfileDashboard = doc.getDocument().toObject(ContentProfileDashboard.class);
                                contentListDashboard.add(contentProfileDashboard);

                                //adapterSeen.notifyDataSetChanged();
                                adapterProfileDashboard.notifyDataSetChanged();

                            }
                        }
                    }
                }
            });
        }
   }

any question?

1
votes

I have found mistake. The working code is:

private void addMessagesEventListener(boolean firstTime) {
    CollectionReference messagesCollection = chatsCollection.document(chat.getId()).collection(Constants.FIREBASE_MESSAGES_PATH);
    Query query = messagesCollection.orderBy("timestamp", Query.Direction.DESCENDING);
    if (!firstTime) {
        query = query.startAt(startListen);
    }
    query.limit(20).get().addOnSuccessListener(queryDocumentSnapshots -> {
        if (!firstTime) {
            endListen = startListen;
        }
        startListen = queryDocumentSnapshots.getDocuments().get(queryDocumentSnapshots.size() - 1);

        Query innerQuery = messagesCollection.orderBy("timestamp").startAt(startListen);
        if(!firstTime) {
            innerQuery = innerQuery.endBefore(endListen);
        }
        ListenerRegistration listener = innerQuery
                .addSnapshotListener((queryDocumentSnapshots1, e) -> {
                    if (e != null) {
                        Log.w("SASA", "listen:error", e);
                        return;
                    }

                    for (DocumentChange dc : queryDocumentSnapshots1.getDocumentChanges()) {
                        Message message = dc.getDocument().toObject(Message.class);
                        switch (dc.getType()) {
                            case ADDED:
                                // add new message to list
                                messageListAdapter.addMessage(message);
                                if (firstTime) {
                                    messagesList.smoothScrollToPosition(0);
                                }
                                break;
                            case REMOVED:
                                // remove message from list
                                messageListAdapter.removeMessage(message);
                                break;
                        }
                    }
                });
        listeners.add(listener);
    });
}

The mistake was in query = query.startAt(startListen) and innerQuery = innerQuery.endBefore(endListen)

  • startListen & endListen are of DocumentSnapshot type
  • I am using sortedList in my adapter

You shoud add

private void detachListeners() {
    for(ListenerRegistration registration : listeners) {
        registration.remove();
    }
}

in onDestroy to detach all listeners.

Code is listening for adding new messages and deleting old ones.