Recyclerview onCreateViewHolder called for every item
Asked Answered
A

1

8

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.

Apprehend answered 17/2, 2018 at 17:8 Comment(1)
dont make the viewholder static..Mustache
S
14

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.

Scholasticate answered 17/2, 2018 at 17:43 Comment(3)
The problem is that there are noticeable lags even with few items, like 10. So even if I set the recyclerview as you said there would be lags I guess.Apprehend
I have gone throught the entire internet for an explanation. Finally, your answer nails it! Particularly, the Android framework only knows that you have a RecyclerView large enough /.../ it has to create a viewholder for every single item ... Now that we know this, is there any way to enable recycling inside a NestedScrollView? Perhaps by resizing the RecyclerView on some scroll callbacks?Elyseelysee
Actually, Android docs say you should Never add a RecyclerView or ListView to a scroll view. Doing so results in poor user interface performance and a poor user experience.Mundy

© 2022 - 2024 — McMap. All rights reserved.