I have an app where user can post item displayed in RecyclerView where Cloud FireStore is the backend. Now in first launch of the app it will load first 5 items. If RecyclerView cannot scroll vertically anymore it will fetch another 5 items. Everything is working except when I delete one item, other item gets duplicate.
First Problem Scenario:
In first launch it will load 5 items, so my list only have 5 items at this moment. Since my pagination limit is 5 when I delete one item from that 5 items, the query listeners tries to load the 6th item. In that part when I scroll up to load the next 5 items I'll get duplicate 6th item.
Like this
1, 2, 3, 4, 5
then the 3rd item will be deleted
1, 2, 4, 5
should be the result
Unfortunately this is what I get 1, 2, 4, 5, 6
. Its good to see that the query itself tries to load another 1 item after 1 item is deleted but after scrolling up the RecyclerView it will load another 5 items. Then this is what I get
1, 2, 4, 5, 6, 6, 7, 8, 9, 10
But since every new added item is going displayed at the top which is 0 index
so it means that what I really see in my list is 6, 1, 2, 4, 5, 6, 7, 8, 9, 10
.
My idea: Do I need to update my DocumentSnapshot lastSeen value too in every delete action or should I adjust the value of limit() dynamically? Please enlighten me of what is the best way to deal with it.
Sample code:
//Load the first item(s) to display
//Set a query according to time in milliseconds
mQuery = mDatabase.collection("Announcements")
.orderBy("time", Query.Direction.DESCENDING)
.limit(5);
//Getting all documents under Announcement collection with query's condition
annon_listener = mQuery.addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(final QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
//If something went wrong
if (e != null)
Log.w(TAG, "Listen failed.", e);
//If any post exist put it to model and add it to List to populate the CardView
//If data exist in the first 5 items then item should be loaded making our 'isFirstListLoaded' variable to be true
if (documentSnapshots != null && !documentSnapshots.isEmpty()){
//If first item are loaded then every update post should be on the top not at the bottom
//This can only be called once to avoid confusion/duplication getting new item
if (isFirstListLoaded){
//Get the documents of last item listed in our RecyclerView
mLastSeen = documentSnapshots.getDocuments().get(documentSnapshots.size()-1);
//Clear the list first to get a latest data
announcementList.clear();
}
//Loop to read each document
for (DocumentChange doc : documentSnapshots.getDocumentChanges()){
//Only added document will be read
switch (doc.getType()){
case ADDED:
//This can only be called once to avoid confusion getting new item(s)
if (isFirstListLoaded){
//Call the model to populate it with document
AnnouncementModel annonPost = doc.getDocument().toObject(AnnouncementModel.class)
.withId(doc.getDocument().getId());
announcementList.add(annonPost);
announcementRecyclerAdapter.notifyDataSetChanged();
noContent.setVisibility(View.GONE);
label.setVisibility(View.VISIBLE);
}
//This will be called once a user added new item to database and put it to top of the list
else if (!isFirstListLoaded){
if (containsLocation(announcementList, doc.getDocument().getId() )){
Log.d(TAG, "Items are gonna duplicate!");
}
else{
//Call the model to populate it with document
AnnouncementModel annonPost = doc.getDocument().toObject(AnnouncementModel.class)
.withId(doc.getDocument().getId());
//This will be called only if user added some new post
announcementList.add(0, annonPost);
announcementRecyclerAdapter.notifyItemInserted(0);
announcementRecyclerAdapter.notifyItemRangeChanged(0, announcementList.size());
}
}
//Just checking of where's the data fetched from
String source = documentSnapshots.getMetadata().isFromCache() ?
"Local" : "Server";
Log.d(TAG, "Data fetched from " + source + "\n" + doc.getDocument().getData());
break;
}
}
//After the first item/latest post was loaded set it to false it means that first items are already fetched
isFirstListLoaded = false;
}
}
});
delete_update_listener = mDatabase.collection("Announcements").addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(@javax.annotation.Nullable QuerySnapshot queryDocumentSnapshots, @javax.annotation.Nullable FirebaseFirestoreException e) {
//If something went wrong
if (e != null)
Log.w(TAG, "Listen failed.", e);
if (queryDocumentSnapshots != null && !queryDocumentSnapshots.isEmpty()) {
//Instead of simply using the entire query snapshot
//See the actual changes to query results between query snapshots (added, removed, and modified)
for (DocumentChange doc : queryDocumentSnapshots.getDocumentChanges()) {
switch (doc.getType()) {
case MODIFIED:
Log.d(TAG, "Modified city: " + doc.getDocument().getData());
break;
case REMOVED:
//Get the document ID of post in FireStore
//Perform a loop and scan the list of announcement to target the correct index
for(int i = 0; i < announcementList.size(); i++) {
//Check if the deleted document ID is equal or exist in the list of announcement
if(doc.getDocument().getId().equals(announcementList.get(i).AnnouncementsID)) {
int prevSize = announcementList.size();
//If yes then delete that object in list by targeting its index
Log.d(TAG, "Removed city: " + announcementList.get(i).getTitle());
announcementList.remove(i);
//Notify the adapter that some item gets remove
announcementRecyclerAdapter.notifyItemRemoved(i);
announcementRecyclerAdapter.notifyItemRangeChanged(i,prevSize-i);
break;
}
}
break;
}
}
}
}
});
//Load more queries
private void loadMoreList() {
//Load the next item(s) to display
//Set a query according to time in milliseconds
//This time start getting data AFTER the last item(s) loaded
if (mLastSeen != null)
mQuery = mDatabase.collection("Announcements")
.orderBy("time", Query.Direction.DESCENDING)
.startAfter(mLastSeen)
.limit(5);
//Getting all documents under Announcement collection with query's condition
annon_listener = mQuery.addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(final QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
//If something went wrong
if (e != null)
Log.w(TAG, "Listen failed.", e);
if (documentSnapshots != null && !documentSnapshots.isEmpty()) {
//If more data exist then update our 'mLastSeen' data
//Update the last list shown in our RecyclerView
mLastSeen = documentSnapshots.getDocuments().get(documentSnapshots.size() - 1);
//Loop to read each document
for (DocumentChange doc : documentSnapshots.getDocumentChanges()) {
//Only added document will be read
switch (doc.getType()) {
case ADDED:
//Call the model to repopulate it with document
AnnouncementModel annonPost = doc.getDocument().toObject(AnnouncementModel.class)
.withId(doc.getDocument().getId());
int prevSize = announcementList.size();
//Add any new item(s) to the List
announcementList.add(annonPost);
//Update the Recycler adapter that new data is added
//This trick performs recycling even though we set nested scroll to false
announcementRecyclerAdapter.notifyItemInserted(prevSize);
announcementRecyclerAdapter.notifyItemRangeInserted(prevSize, 5);
//Just checking of where's the data fetched from
String source = documentSnapshots.getMetadata().isFromCache() ?
"Local" : "Server";
Log.d(TAG, "Data LOADED from " + source + "\n" + doc.getDocument().getData());
break;
case REMOVED:
break;
case MODIFIED:
break;
}
}
}
//If no more item(s) to load
else if (!isDetached() && getContext() != null)
StyleableToast.makeText(getContext(), "All items are loaded.", R.style.mytoastNoItems).show();
}
});
}
Additionally I tried to observe how doc types work "ADDED" , "REMOVED", and "MODIFIED". If I put "REMOVED" also inside the listeners that is using the query,the REMOVED is the one that is being called first followed by ADDED when adding new items which will cause more trouble.