我有一个应用程序,用户可以在其中发布在RecyclerView中显示的项目,其中Cloud FireStore是后端。现在,在第一次启动应用程序时,它将加载前5个项目。如果RecyclerView不能再垂直滚动,它将获取另外5个项目。一切正常,除非我删除一个项目,其他项目会重复。
第一个问题场景:
首次启动时会加载5个项目,所以此刻我的列表只有5个项目。由于当我从该5个项目中删除一个项目时,我的分页限制为5,因此查询侦听器会尝试加载第6个项目。在那个部分,当我向上滚动以加载接下来的5个项目时,我将得到重复的第6个项目。
喜欢这个
1, 2, 3, 4, 5
然后第3项将被删除
1, 2, 4, 5
应该是结果
不幸的是,这就是我得到的1, 2, 4, 5, 6
。很高兴看到查询本身在删除1项后尝试加载另外1项,但在向上滚动RecyclerView后,它将加载另外5项。那就是我得到的
1, 2, 4, 5, 6, 6, 7, 8, 9, 10
但是,因为每个新添加的项目都会显示在顶部0 index
,所以这意味着我在列表中看到的是6, 1, 2, 4, 5, 6, 7, 8, 9, 10
。
我的想法:我是否需要在每次删除操作中更新我的DocumentSnapshot lastSeen值,还是应该动态调整limit()的值? 请告诉我处理它的最佳方法是什么。
示例代码:
//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();
}
});
}
此外,我试着观察doc类型如何工作“ADDED”,“REMOVED”和“MODIFIED”。如果我在使用查询的侦听器中也放置了“REMOVED”,则REMOVED是在添加新项目时首先被调用,然后是ADDED,这将导致更多麻烦。
答案 0 :(得分:2)
所以最后几个月后,我找到了使用RecyclerView使用FireStore执行实时更新的最佳方法和最佳方法。在Alex Mamo的回答here
的帮助下最好的方法是获取数据/文档一次,然后为该集合提供一个ListenerRegistration。这是我的解决方案。
首先是初始化成员布尔变量并将其设置为 true。 这是必要的,因为在第一次启动时会触发文档类型 ADDED ,而我们不需要。
private boolean isFirstListLoaded = true;
接下来是声明你的ListenerRegistration,这是一个可选的,但我强烈建议你提供一个监听器,这样你就不再需要在 addSnapshotListener 中包含'this'了 strong>参数。将'this'包含在参数中会为数据节省一些内存,但有时会停止实时功能,因为它依赖于碎片或活动生命周期,这会破坏实时更新的目的
private ListenerRegistration update_listener
然后像这样创建你的查询
private Query mQuery;
升序或降序和限制取决于你。
将它放在 onCreate 方法上,这样它只会运行一次。
mQuery= mDatabase.collection("Your Collection")
.orderBy("some fields within each document like names or time", Query.Direction.DESCENDING)
.limit(5);
//Run the first query in the beginning
mQuery.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
if (!task.getResult().isEmpty()) {
//Get the documents of last item listed in our RecyclerView
mLastSeen = task.getResult().getDocuments().get(task.getResult().size() - 1);
//Loop to read each document
for (DocumentSnapshot document : task.getResult()) {
//Call the model to populate it with document
Model model = Objects.requireNonNull(document.toObject(Model .class))
.withId(document.getId());
//Add every item/document to the list
mList.add(model);
//Notify the adapter that new item is added
yourRecyclerAdapter.notifyItemInserted(mList.size());
noContent.setVisibility(View.GONE);
label.setVisibility(View.VISIBLE);
//Just checking of where's the data fetched from
String source = document.getMetadata().isFromCache() ?
"Local" : "Server";
Log.d(TAG, "Data fetched from " + source + "\n" + document.getData());
}
}
//If task is successful even though there's no existing item yet, then first fetch is success
isFirstListLoaded = false;
}
else if (getContext() != null)
Toast.makeText(getContext(),"Error: "+ Objects.requireNonNull(task.getException()).getMessage(),Toast.LENGTH_LONG).show();
}
});
也是这个。
//Listener
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) {
//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 ADDED:
if (!isFirstListLoaded){
//Call the model to populate it with document
Model model= doc.getDocument().toObject(Model.class)
.withId(doc.getDocument().getId());
//This will be called only if user added some new post
mList.add(0, model);
yourRecyclerAdapter.notifyItemInserted(0);
yourRecyclerAdapter.notifyItemRangeChanged(0, announcementList.size());
}
break;
case MODIFIED:
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)) {
//If yes then delete that object in list by targeting its index
Log.d(TAG, "Removed Post: " + announcementList.get(i).getTitle());
announcementList.remove(i);
//Notify the adapter that some item gets remove
announcementRecyclerAdapter.notifyItemRemoved(i);
announcementRecyclerAdapter.notifyItemRangeChanged(i, announcementList.size());
break;
}
}
break;
}
}
isFirstListLoaded = false;
}
}
});
然后,只要您想加载更多项目,请调用此方法。
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 (mAnnonLastSeen != null)
mAnnouncementQuery = mDatabase.collection("Your collection")
.orderBy("some field within your documents", Query.Direction.DESCENDING)
.startAfter(mLastSeen)
.limit(5);
mAnnouncementQuery.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
if (!task.getResult().isEmpty()) {
//Get the documents of last item listed in our RecyclerView
mLastSeen = task.getResult().getDocuments().get(task.getResult().size() - 1);
//Loop to read each document
for (DocumentSnapshot document : task.getResult()) {
//Call the model to populate it with document
AnnouncementModel annonPost = Objects.requireNonNull(document.toObject(AnnouncementModel.class))
.withId(document.getId());
//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(announcementList.size());
//Just checking of where's the data fetched from
String source = document.getMetadata().isFromCache() ?
"Local" : "Server";
Log.d(TAG, "Data fetched from " + source + "\n" + document.getData());
}
} else if (!isDetached() && getContext() != null)
Toast.makeText(getContext(), "All items are loaded.", Toast.LENGTH_LONG).show();
}
else if (getContext() != null)
Toast.makeText(getContext(),"Error: "+ Objects.requireNonNull(task.getException()).getMessage(), Toast.LENGTH_LONG).show();
}
});
}
其中mLastSeen
是成员varialble DocumentSnapshot。干杯!
答案 1 :(得分:0)
您似乎正在从细分和每个细分中构建列表:
在这种情况下,从细分中删除项目会导致对下一个细分的起始文档进行其他更改。
虽然它对服务器的负载不是太糟糕,但由于大多数文档都来自本地缓存,这确实导致了一些文件的混乱。
因此,您会发现许多开发人员采用其他方法。我看到的最常见的是只有一个段,只是在用户向下滚动时增加限制。因此查询最初有5个项目,然后是10项,然后是15项等。
更复杂的情况是将每个段锚定在起始文档和结束文档。这样,从段内删除文档,不会更改其周围的其他段。但是这种情况还有其他一些问题,所以我一定会先找一些比较有名的东西。