Firestore Firebase Pagination в RecyclerView не работает должным образом после удаления некоторых элементов - PullRequest
0 голосов
/ 12 июня 2018

У меня есть приложение, в котором пользователь может разместить элемент, отображаемый в RecyclerView, где Cloud FireStore является бэкэндом.Теперь при первом запуске приложения оно загрузит первые 5 элементов.Если RecyclerView больше не может прокручивать по вертикали, он получит еще 5 элементов.Все работает, за исключением случаев, когда я удаляю один элемент, другой элемент дублируется.

Первый сценарий проблемы:

При первом запуске загружается 5 элементов, поэтому в моем списке только 5 элементов на данный момент,Так как мой предел нумерации страниц равен 5, когда я удаляю один элемент из этих 5 элементов, слушатели запросов пытаются загрузить 6-й элемент.В этой части, когда я прокручиваю вверх, чтобы загрузить следующие 5 элементов, я получу дубликат 6-го элемента.

Как это 1, 2, 3, 4, 5, тогда третий элемент будет удален 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.

Моя идея: нужно ли мне обновлять значение lastSeen для DocumentSnapshot в каждом действии удаления или мне нужно динамически корректировать значение 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();

            }
        });
    }

Кроме того, я попытался наблюдать, как работают типы документов "ДОБАВЛЕНО", "УДАЛЕНО" и«МОДИФИЦИРОВАННЫЙ».Если я добавлю «REMOVED» также внутри слушателей, использующих запрос, то REMOVED - это тот, который вызывается первым, а затем «ADDED» при добавлении новых элементов, что вызовет больше проблем.

Ответы [ 2 ]

0 голосов
/ 13 июня 2018

Итак, наконец, через несколько месяцев я выясняю, как лучше и лучше всего выполнять обновления в режиме реального времени с помощью FireStore с RecyclerView.С помощью ответа Алекса Мамо здесь

Лучший способ - получить данные / документ один раз, а затем предоставить ListenerRegistration с этой коллекцией.Вот мое решение.

Сначала нужно инициализировать логическую переменную члена и установить для нее значение true. Это необходимо, потому что тип документа ADDED запускается в первомзапустите, и нам это не нужно.

private boolean isFirstListLoaded = true;

Далее следует объявить вашу ListenerRegistration, это необязательно, но я настоятельно рекомендую предоставить прослушиватель, чтобы вам больше не понадобилось вводить 'this' в параметре addSnapshotListener .Включение 'this' в параметр сэкономит вам некоторую память в данных, но также иногда остановит функцию реального времени, поскольку это зависит от фрагмента или жизненного цикла действия, которые разрушают цель получения обновлений в реальном времени.

 private ListenerRegistration update_listener

Затем создайте свои запросы следующим образом:

 private Query mQuery;

По возрастанию или по убыванию, и ограничение зависит от вас.

Поместите это в onCreate метод, поэтому он будет запускаться только 1 раз.

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 - переменный элемент DocumentSnapshot.Ура!

0 голосов
/ 12 июня 2018

Похоже, вы строите список из сегментов, и каждый сегмент:

  • начинается с определенного документа
  • содержит следующие 5 элементов

В этом случае удаление элемента из сегмента приводит к дополнительному изменению начального документа следующего сегмента.

Хотя это не так уж плохо при загрузке с сервера, так как большинство документов будетесли исходить из локального кэша, это приводит к некоторой перестановке документов.

По этой причине вы найдете много разработчиков, использующих альтернативные подходы.Наиболее распространенный вариант, который я вижу, - это иметь только один сегмент и просто увеличивать лимит при прокрутке пользователя вниз.Таким образом, запрос изначально имеет 5 элементов, затем 10, затем 15 и т. Д.

Более сложный сценарий заключается в привязке каждого сегмента к начальному документу и к конечному документу.Таким образом, удаление документа из сегмента не изменит другие сегменты вокруг него.Но у этого сценария есть другие осложнения, поэтому я бы определенно остановился на чем-то более известном.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...