FirebaseRecyclerAdapter с двумя разными ссылками на базу данных - негативное влияние на прокрутку

введите здесь описание изображения

Простая вещь, которую я хотел бы сделать (см. на картинке)

Отображать представление с информацией, поступающей из 2 разных мест в Firebase, чтобы оно вело себя профессионально, прокручивая ВВЕРХ и ВНИЗ

У меня есть список фильмов и по каждому из них я бы хотел, чтобы пользователь указал рейтинг и увидел его

В БД я создал 2 структуры, чтобы иметь список фильмов с одной стороны и рейтинги для каждого пользователя с другой.

Проблема с использованием FirebaseRecyclerAdapter

Моя проблема в том, что при быстрой прокрутке вверх и вниз по списку визуализация информации, поступающей от второй ссылки (рейтинг), загружается в разное время (асинхронный вызов), и это неприемлемо для просмотра. это (небольшая) задержка построения представления. Является ли это ограничением FirebaseRecyclerView?

Поскольку viewHolder повторно используются в recycleView, я сбрасываю и перезагружаю каждый раз в populateView() значения рейтинга, и это не помогает. После получения я обязан получить их снова, если пользователь прокручивает представление (см. setOnlistener в populateView()

Установка прослушивателя в populateView также приводит к тому, что количество прослушивателей будет равно количеству выполнений populateView() (если вы прокручиваете ВВЕРХ и ВНИЗ, это будет много раз).

Решения / Обходной путь?

Есть ли правильный способ сделать это, чтобы предотвратить проблему? Или это ограничение? Как насчет производительности с моей реализацией, где прослушиватель находится внутри populateView() и создано МНОГОЕ прослушивателей?

Ниже некоторые вещи, о которых я думаю:

  • Предотвратить повторное использование viewHolder и просто загрузить один раз?
  • Переопределить некоторые другие методы RecyclerView? Я пробовал использовать parseSnapshot(), но та же проблема...
  • Измените структуру БД, чтобы вся информация была в одном списке (я не думаю, что это хорошо, потому что это означает добавление информации о рейтинге каждого пользователя в список фильмов)
  • Добавьте счетчик загрузки в часть рейтинга, чтобы рейтинг отображался только после завершения асинхронного вызова firebase (не нравится) без эффекта сегодняшнего дня: «изменение цвета звезды перед пользователем».

Моя реализация

Из FirebaseRecyclerAdapter

 @Override
protected void populateViewHolder(final MovieViewHolder viewHolder, final     Movie movie, final int position) {

    String movieId = this.getRef(position).getKey();

    // Oblidged to show no rating at the beginning because otherwise
    // if a viewHolder is reused it has the values from another movie
    viewHolder.showNoRating();

    //---------------------------------------------
    // Set values in the viewHolder from the model 
    //---------------------------------------------
    viewHolder.movieTitle.setText(movie.getTitle());
    viewHolder.movieDescription.setText(movie.getDescription());

    //-----------------------------------------------------
    // Ratings info are in another DB location... get them
    // but call is asynchronous so PROBLEM when SCROLLING!
    //-----------------------------------------------------
    DatabaseReference ratingMovieRef = mDbRef.child(Constants.FIREBASE_LOCATION_RATINGS).child(currentUserId).child(movieId);
 ratingQuoteRef.addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {


            RatingMovie ratingMovie = dataSnapshot.getValue(RatingMovie.class);
            Rating rating = Rating.NO_RATING;
            if (ratingMovie != null) {
                rating = Rating.valueOf(ratingMovie.getRating());
            }

            // Set the rating in the viewholder (through anhelper method)
            viewHolder.showActiveRating(rating);
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {

        }
    });

}

из MovieViewHolder

public class QuoteViewHolder extends RecyclerView.ViewHolder {

public CardView cardView;
public TextView movieTitle;
public TextView movieDescription;
public ImageView ratingOneStar;
public ImageView ratingTwoStar;
public ImageView ratingThreeStar;

public QuoteViewHolder(View itemView) {

    super(itemView);
    movieTitle = (TextView)itemView.findViewById(R.id.movie_title);
    movieDescription = (TextView)itemView.findViewById(R.id.movie_descr);

    // rating
    ratingOneStar = (ImageView)itemView.findViewById(R.id.rating_one);
    ratingTwoStar = (ImageView)itemView.findViewById(R.id.rating_two);
    ratingThreeStar = (ImageView)itemView.findViewById(R.id.rating_three);
}

/**
* Helper to show the color on stars depending on rating value 
*/
public void showActiveRating(Rating rating){

    if (rating.equals(Rating.ONE)) {
        // just set the good color on ratingOneStar and the others
        ...
    }
    else if (rating.equals(Rating.TWO)) {
        // just set the good color
        ...
    } else if (rating.equals(Rating.THREE)) {
       // just set the good color
       ...
   }


/**
 * Initialize the rating icons to unselected.
 * Important because the view holder can be reused and if not initalised values from other moviecan be seen
 */
public void initialiseNoRating(){
 ratingOneStar.setColorFilter(ContextCompat.getColor(itemView.getContext(), R.color.light_grey)); 
    ratingTwoStar.setColorFilter(....
    ratingThreeStar.SetColorFilter(...
}



Ответы (1)


Вы можете кэшировать рейтинги, используя ChildEventListener. По сути, просто создайте отдельный узел только для узла «Рейтинги» и сохраните рейтинги на карте. Затем, используя RecyclerAdapter, вы получите доступ к карте, если рейтинг доступен, если нет, пусть прослушиватель рейтинга обновит просмотр recyclerview, как только он загрузит рейтинг. Это одна из стратегий, которую вы можете использовать. При этом вам придется вручную скопировать/вставить некоторые классы из библиотеки FirebaseUI и сделать некоторые поля общедоступными, чтобы это работало.

Использование будет примерно таким

private MovieRatingConnection ratingConnection;

   // inside onCreate

    ratingConnection = new MovieRatingConnection(userId, new MovieRatingConnection.RatingChangeListener() {
        @Override
        public void onRatingChanged(DataSnapshot dataSnapshot) {
            if (recyclerAdapter != null) {
                if (dataSnapshot != null) {
                    int index = recyclerAdapter.snapshots.getIndexForKey(dataSnapshot.getKey());
                    recyclerAdapter.notifyItemChanged(index);
                }
            }
        }
    });

    Query movieQuery = FirebaseDatabase.getInstance().getReference().child("Movies");
    recyclerAdapter = new FirebaseRecyclerAdapter(movieQuery...) {
        @Override
        public void populateViewHolder(RecyclerView.ViewHolder viewHolder, Object model, int position) {
            //...
            final String key = getRef(position).getKey();
            viewHolder.showActiveRating(ratingConnection.getRating(key));
        }
    };

и MovieRatingConnection будет таким классом

public class MovieRatingConnection {

    private MovieRatingListener listener;

    public MovieRatingConnection(String userId, RatingChangeListener changeListener) {
        Query query = FirebaseDatabase.getInstance().getReference().child("MovieRatings").child(userId);
        listener = new MovieRatingListener(query, changeListener);
    }

    public Rating getRating(String key) {
        return listener.getRating(key);
    }

    public void cleanup() {
        if (listener != null) {
            listener.unregister();
        }
    }



    public static class MovieRatingListener implements ChildEventListener {

        public interface RatingChangeListener {
            public void onRatingChanged(DataSnapshot snapshot);

        }

        private Query query;
        private HashMap<String, Rating> ratingMap = new HashMap<>();
        private RatingChangeListener changeListener;


        public MovieRatingListener(Query query, RatingChangeListener changeListener) {
            this.query = query;
            this.changeListener = changeListener;
            query.addChildEventListener(this);

        }

        @Override
        public void onChildAdded(DataSnapshot dataSnapshot, String s) {
            if (dataSnapshot != null) {
                ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
                changeListener.onRatingChanged(dataSnapshot);
            }
        }

        @Override
        public void onChildChanged(DataSnapshot dataSnapshot, String s) {
            if (dataSnapshot != null) {
                ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
                changeListener.onRatingChanged(dataSnapshot);
            }
        }

        @Override
        public void onChildRemoved(DataSnapshot dataSnapshot) {
            ratingMap.remove(dataSnapshot.getKey());
            changeListener.onRatingChanged(null);
        }

        @Override
        public void onChildMoved(DataSnapshot dataSnapshot, String s) {

        }

        @Override
        public void onCancelled(DatabaseError databaseError) {

        }

        public Rating getRating(String key) {
            if (ratingMap.get(key) != null) {
                return ratingMap.get(key);
            } else {
                return new Rating(); // default value/null object
            }
        }

        public void unregister() {
            query.removeEventListener(this);
        }
    }
}
person Linxy    schedule 27.02.2017
comment
Спасибо. Означает ли это, что через стандартную библиотеку FirebaseUI без ее изменения нельзя? Если я хорошо понял, основной смысл вашей идеи таков: создайте и прикрепите прослушиватель к рейтингам и сохраните значения на карте, чтобы они потенциально были уже доступны при выполнении populateView() (хорошо). Но мне не ясно: int index = recyclerAdapter.snapshots.getIndexForKey(dataSnapshot.getKey()); recyclerAdapter.snapshots.mSnapshots.set(index, dataSnapshot); dataSnapshot исходит из рейтинга, так как я могу обновить фильм? - person DavideN; 27.02.2017
comment
Ах да, извините, да, просто удалите эти строки и вызовите notifyItemChanged(index) без набора. Скорее всего, вы просто хотите сказать переработчику перезагрузить этот предмет. - person Linxy; 27.02.2017
comment
Я понимаю, что нужно как можно скорее загрузить информацию о рейтинге, чтобы она была доступна в populateView. Это можно сделать, вызвав слушателя в действии, но какой смысл в changeListener.onRatingChanged(dataSnapshot) вызове? Для меня есть 2 случая (1), когда я отображаю список фильмов через populateViewHolder(), рейтинги уже находятся в кеше, поэтому проблем с прокруткой нет. (2) Значения все еще отсутствуют или были обновлены (невозможно в этом сценарии), и я уведомляю адаптер фильма ==> в этом случае проблема отображения поддельного значения до тех пор, пока оно не будет доступно... - person DavideN; 27.02.2017
comment
RatingChangedListener — это просто удобный обратный вызов для вашей активности, чтобы сигнализировать о том, что рейтинг был добавлен/обновлен/удален, и recyclerview должен отражать это, поскольку он не прослушивает рейтинги, а просто получает их с локальной карты. Я не совсем уверен, что вы имеете в виду в своем случае 2. - person Linxy; 28.02.2017