Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recyclerview onCreateViewHolder called for every item

I have a RecyclerView inside a NestedScrollView that show some data downloaded asynchronously. The problem is that there is a significant lag when the items are initilized. After some tests I found out that the problem is that onCreateViewHolder is called for every item and it took some time to inflate the layout. This is my adapter:

public class EpisodeAdapter extends RecyclerView.Adapter<EpisodeAdapter.ViewHolder> {

    private static final String TAG = "EpisodeAdapter";

    private static final int NO_POSITION = -1;
    private static final int EXPAND = 1;
    private static final int COLLAPSE = 2;

    private SparseArray<Episode> episodes;
    private OnItemClickListener<Episode> downloadClickListener;
    private OnItemClickListener<Episode> playClickListener;

    private RecyclerView recyclerView;
    private final EpisodeAnimator episodeAnimator;
    private final Transition expandCollapse;

    private int expandedPosition = NO_POSITION;

    public EpisodeAdapter() {
        episodes = new SparseArray<>();
        episodeAnimator = new EpisodeAnimator();
        expandCollapse = new AutoTransition();
    }

    //Called when first loading items
    public void swapEpisodes(SparseArray<Episode> newEpisodes){
        final int previousSize = episodes.size();
        episodes = newEpisodes;
        expandedPosition = NO_POSITION;
        Log.e(TAG, "Swap called");
        if(previousSize == 0) {
            notifyItemRangeInserted(0, episodes.size());
        }
        else {
            notifyItemRangeChanged(0, Math.max(previousSize, episodes.size()));
        }
    }

    //Called when downloading other information, this seems to work fine without delay
    public void setEpisodesDetails(final List<TmdbEpisode> episodeList){
        for (TmdbEpisode episode : episodeList){
            final int position = episodes.indexOfKey(episode.getNumber());
            notifyItemChanged(position, episode);
        }
    }

    @Override
    public EpisodeAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Log.e(TAG, "Start createViewHolder");
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_episode, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);

        viewHolder.downloadButton.setOnClickListener(v -> {
            if(downloadClickListener != null)
                downloadClickListener.onItemClick(v, episodes.valueAt(viewHolder.getAdapterPosition()));
        });

        viewHolder.playButton.setOnClickListener(v -> {
            if(playClickListener != null)
                playClickListener.onItemClick(v, episodes.valueAt(viewHolder.getAdapterPosition()));
        });

        viewHolder.itemView.setOnClickListener(v -> {
            final int position = viewHolder.getAdapterPosition();
            if(position == NO_POSITION) return;

            TransitionManager.beginDelayedTransition(recyclerView, expandCollapse);
            episodeAnimator.setAnimateMoves(false);

            //Collapse any currently expanded items
            if(expandedPosition != NO_POSITION){
                notifyItemChanged(expandedPosition, COLLAPSE);
            }

            //Expand clicked item
            if(expandedPosition != position){
                expandedPosition = position;
                notifyItemChanged(position, EXPAND);
            }
            else {
                expandedPosition = NO_POSITION;
            }
        });

        Log.e(TAG, "Finish createViewHolder");
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(EpisodeAdapter.ViewHolder holder, int itemPosition) {
        Log.e(TAG, "Start");
        holder.number.setText(String.valueOf(episodes.keyAt(itemPosition)));
        holder.details.setVisibility(View.GONE);
        holder.itemView.setActivated(false);
        Log.e(TAG, "Finish");
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads) {
        Log.e(TAG, "Start payloads");
        if(payloads.contains(EXPAND) || payloads.contains(COLLAPSE)){
            setExpanded(holder, position == expandedPosition);
        }
        else if(!payloads.isEmpty() && payloads.get(0) instanceof TmdbEpisode){
                TmdbEpisode episode = (TmdbEpisode) payloads.get(0);
                holder.title.setText(episode.getName());
                holder.details.setText(episode.getOverview());
        }
        else {
            onBindViewHolder(holder, position);
        }
        Log.e(TAG, "Finish payloads");
    }

    private void setExpanded(ViewHolder holder, boolean isExpanded) {
        holder.itemView.setActivated(isExpanded);
        holder.details.setVisibility((isExpanded) ? View.VISIBLE : View.GONE);
    }


    public void setPlayClickListener(OnItemClickListener<Episode> onItemClickListener){
        playClickListener = onItemClickListener;
    }

    public void setDownloadClickListener(OnItemClickListener<Episode> onItemClickListener){
        downloadClickListener = onItemClickListener;
    }

    @Override
    public int getItemCount() {
        return episodes.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        View itemView;
        TextView number;
        FadeTextSwitcher title;
        ImageButton downloadButton;
        FloatingActionButton playButton;
        TextView details;

        ViewHolder(View itemView) {
            super(itemView);
            Log.e(TAG, "Start constructor");
            this.itemView = itemView;
            number = itemView.findViewById(R.id.number);
            title = itemView.findViewById(R.id.title);
            downloadButton = itemView.findViewById(R.id.download_button);
            playButton = itemView.findViewById(R.id.play_button);
            details = itemView.findViewById(R.id.details);
            Log.e(TAG, "Finish constructor");
        }
    }

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);
    this.recyclerView = recyclerView;
    this.recyclerView.setItemAnimator(episodeAnimator);

    expandCollapse.setDuration(recyclerView.getContext().getResources().getInteger(R.integer.episode_expand_collapse_duration));
    expandCollapse.setInterpolator(AnimationUtils.loadInterpolator(this.recyclerView.getContext(), android.R.interpolator.fast_out_slow_in));
    expandCollapse.addListener(new Transition.TransitionListener() {
        @Override
        public void onTransitionStart(android.transition.Transition transition) {
           EpisodeAdapter.this.recyclerView.setOnTouchListener((v, event) -> true);
        }

        @Override
        public void onTransitionEnd(android.transition.Transition transition) {
           episodeAnimator.setAnimateMoves(true);
           EpisodeAdapter.this.recyclerView.setOnTouchListener(null);
        }

        @Override
        public void onTransitionCancel(android.transition.Transition transition) {}

        @Override
        public void onTransitionPause(android.transition.Transition transition) {}

        @Override
        public void onTransitionResume(android.transition.Transition transition) {}
    });
}

static class EpisodeAnimator extends SlideInItemAnimator {
    private boolean animateMoves = false;

    EpisodeAnimator() {
        super();
    }

    void setAnimateMoves(boolean animateMoves) {
        this.animateMoves = animateMoves;
    }

    @Override
    public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        if (!animateMoves) {
            dispatchMoveFinished(holder);
            return false;
        }
        return super.animateMove(holder, fromX, fromY, toX, toY);
    }
}
}

Is there a way to force reusing the same ViewHolder for every items? So onCreateViewHolder will be called once.

I've also set nestedScrollingEnabled="false" in the recyclerview.

like image 302
Matteo Destro Avatar asked Feb 17 '18 17:02

Matteo Destro


People also ask

How many times onCreateViewHolder is called in RecyclerView?

By default it have 5. you can increase as per your need. Save this answer.

How often is onBindViewHolder called?

However, in RecyclerView the onBindViewHolder gets called every time the ViewHolder is bound and the setOnClickListener will be triggered too. Therefore, setting a click listener in onCreateViewHolder which invokes only when a ViewHolder gets created is preferable.

What is ViewHolder in RecyclerView?

A ViewHolder describes an item view and metadata about its place within the RecyclerView. Adapter implementations should subclass ViewHolder and add fields for caching potentially expensive findViewById results. While LayoutParams belong to the LayoutManager , ViewHolders belong to the adapter.

What are the three mandatory methods of a RecyclerView?

These required methods are as follows: onCreateViewHolder(ViewGroup parent, int viewType) onBindViewHolder(RecyclerView. ViewHolder holder, int position)


1 Answers

I have a RecyclerView inside a NestedScrollView

I'm going to guess that your <RecyclerView> tag has its height defined as wrap_content. If it does, that means that you're inflating a layout resource (and creating a ViewHolder object) for every single item in your data set; potentially thousands of layout inflations and object creations.

The recycling behavior of RecyclerView only works when the height of the recyclerview is smaller than the height needed to display its children. It's normal for a recyclerview to create a small double-digit number of ViewHolder instances (usually however many items you can see on screen at once plus a few to optimize views just off-screen), but this depends on the fact that your recyclerview's size is constrained by the screen size (i.e. you're using match_parent or a fixed size).

In the case of a RecyclerView with wrap_content height inside a NestedScrollView, the user won't be able to see all of the items at a single time, but the Android framework only knows that you have a recyclerview large enough to hold every single item in your data set and so it has to create a viewholder for every single item.

You'll have to figure out a way to rework your layout hierarchy so that you can use some limited height for your RecyclerView.

like image 98
Ben P. Avatar answered Oct 14 '22 16:10

Ben P.