4

enter image description here

Simple thing I would like to do (see in the picture)

Display a view with info coming from 2 different places in Firebase so that it behaves in a professional way scrolling UP and DOWN

I have a list of movies and on each of them I would like the user to specify a rating and see it

In DB I created 2 structures to have the list of movies on one side and the ratings per user on the other

Problem using FirebaseRecyclerAdapter

My problem is that scrolling fast up and down the list, the visualization of the information coming from the second reference (the rating) is loaded on a different time (asynchronous call) and this is not acceptable to see this (little) delay building the view. Is this a limitation of FirebaseRecyclerView?

Because viewHolders are reused in the recycleView I reset and reload each time in populateView() the rating values and this doesn't help. Once retrieved I'm oblidged to get them again if the user scroll the view (see the setOnlistener in populateView()

Setting a listener in populateView cause also to have as many listener as the number of times populateView() is executed (if you scroll UP and DOWN it's many times).

Solutions / Workaround ?

Is there a correct way to do it preventing the problem? Or is it a limitation? What about performance with my implementation where the listener is inside populateView() and there are MANY listener created?

Below some things I'm thinking on:

  • Prevent viewHolders to be recycled and just load once?
  • Override some other methods of RecyclerView? I tried with parseSnapshot() but it's the same problem...
  • Change the DB structure to have all the info in one list (I don't think it's the good one because it means adding rating information of each user to movie list)
  • Add a loading spinner on the rating part so that the rating is displayed only when the asyncrhonous call to firebase is completed (don't like it) without the today effect of: "changing star color in front of the user".

My Implementation

From 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) {

        }
    });

}

from 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(...
}
AL.
  • 36,815
  • 10
  • 142
  • 281
DavideN
  • 253
  • 2
  • 11

1 Answers1

2

You can sort of cache the ratings using a ChildEventListener. Basically just create a separat one just for the Ratings node, and have it store the ratings in a Map. Then using the RecyclerAdapter you will retrieve from the Map if the rating is available, if it is not, have the rating listener update the recyclerview as soon as is has downloaded the rating. This is one strategy you could go about, doing it, you will have to manually copy/paste some classes from the FirebaseUI library and set some fields public for this to work.

Usage would be something like this

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));
        }
    };

and MovieRatingConnection would be a class like this

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);
        }
    }
}
Linxy
  • 2,525
  • 3
  • 22
  • 37
  • 1
    Thanks. Does this mean that is it not possible via the standard FirebaseUI library without changing it? If I well undertood the main point of your idea is: create and attach a listener to ratings and save the values in a map so that they are potentially already available when populateView() is executed (ok). But is not clear for me: `int index = recyclerAdapter.snapshots.getIndexForKey(dataSnapshot.getKey()); recyclerAdapter.snapshots.mSnapshots.set(index, dataSnapshot);` dataSnapshot is coming from rating so how can I update movie? – DavideN Feb 27 '17 at 17:04
  • 1
    Oh right, sorry, yeah just remove those lines and call notifyItemChanged(index), without the set. You baiscally just want to tell the recycler to reload that item,. – Linxy Feb 27 '17 at 17:16
  • 1
    I uderstand the point to load asap the rating info to have them available in populateView. This can be done calling a listener in activity, but what's the point of `changeListener.onRatingChanged(dataSnapshot)` call? For me there are 2 cases (1) when I display the list of movies via populateViewHolder() the ratings are already in cache so no problem in scrolling. (2) The values are not still there or have been updated (not possible in this scenario) and I notify the movie adapter ==> in this case the problem of displaying a "fake" value until available would still be present... – DavideN Feb 27 '17 at 18:13
  • 1
    the RatingChangedListener is just a convenience callback to your activity to signal that a Rating was added/updated/removed, and the recyclerview should reflect that, as it is not listening for the ratings but is just getting them from a local map. Im not quite sure what you mean with your case 2. – Linxy Feb 27 '17 at 21:17