Loading large number of items in recycler view
Asked Answered
P

4

15

I have a recycler view within a fragment and basically I m trying to load song list in the recycler view .Each row of recycler view contains an imageview (for album art) and textview ( for song name). I am having trouble when the size of the dataset is huge, that is when there are too many songs, the recycler view lags and the app ends up giving an ANR.I am using Glide to load album arts in each row's imageview. How is google music player able to show such large number of songs without any lag?

Edit: This is my SongsFragment

public class SongsFragment extends Fragment {
static {
    AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
ProgressBar progressBar;    // progress bar to show after every 30 items
NestedScrollView nestedScrollView;  //for smooth scrolling of recyclerview as well as to detect the end of recyclerview
RecyclerView recyclerView;
ArrayList<Song> songMainList = new ArrayList<>();  //partial list in which items are added
ArrayList<Song> songAllList = new ArrayList<>(); //Complete List of songs
SongAdapter songsAdapter;
private LinearLayoutManager layoutManager;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View rootView = inflater.inflate(R.layout.fragment_songs, container, false);

    nestedScrollView = (NestedScrollView) rootView.findViewById(R.id.nestedScrollView);
    progressBar = (ProgressBar) rootView.findViewById(R.id.progressBar);

    String songJson = getActivity().getIntent().getStringExtra("songList");
    songAllList = new Gson().fromJson(songJson, new TypeToken<ArrayList<Song>>() {
    }.getType());
            //Getting list of all songs in songAllList

    if (songAllList.size() > 30) {  
        songMainList = new ArrayList<>(songAllList.subList(0,30));
    } else {
        songMainList = songAllList;
    }

    //if size of fetched songAllList>30 then add only 30 rows to songMainList

    recyclerView = (RecyclerView) rootView.findViewById(R.id.songs);
    int spanCount = 1; // 2 columns
    int spacing = 4; // 50px
    recyclerView.addItemDecoration(new GridItemDecoration(spanCount, spacing, true));
    recyclerView.setHasFixedSize(true);
    recyclerView.setNestedScrollingEnabled(false);
    recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
    songsAdapter = new SongAdapter(getActivity(), songMainList, recyclerView);

    nestedScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            View view = (View) nestedScrollView.getChildAt(nestedScrollView.getChildCount() - 1);
            int diff = (view.getBottom() - (nestedScrollView.getHeight() + nestedScrollView
                    .getScrollY()));

            if (diff == 0) {    //NestedScrollView scrolled to bottom
                progressBar.setVisibility(View.VISIBLE);    //show progressbar
                new Handler().postDelayed(new Runnable() { 
                    @Override
                    public void run() {
                        if (songMainList.size() < songAllList.size()) {
                            int x = 0, y = 0;
                            if ((songAllList.size() - songMainList.size()) >= 30) {
                                x = songMainList.size();
                                y = x + 30;
                            } else {
                                x = songMainList.size();
                                y = x + songAllList.size() - songMainList.size();
                            }
                            for (int i = x; i < y; i++) {
                                songMainList.add(songAllList.get(i));   //Adding new items from songAllList to songMainList one by one
                                songsAdapter.notifyDataSetChanged();
                            }
                        }
                        progressBar.setVisibility(View.GONE);
                    }
                }, 1500);


            }
        }
    });
    recyclerView.setAdapter(songsAdapter);


    return rootView;
    }
}

And this is my RecyclerViewAdapter along with viewholder

public class SongAdapter extends RecyclerView.Adapter {

private List<Song> songsList;
private Context c;

private RecyclerView.ViewHolder holder;

public SongAdapter(Context context) {
    mainActivityContext = context;
}

public SongAdapter(Context context, List<Song> songs, RecyclerView recyclerView) {
    songsList = songs;
    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
    c = context;
}

public SongAdapter getInstance() {
    return SongAdapter.this;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.song_list_row, parent, false);
    return new SongViewHolder(view,c);
}

@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {

    if (holder instanceof SongViewHolder) {
        Song song = songsList.get(position);
        this.holder = holder;

        String name = song.getName();
        String artist = song.getArtist();
        String imagepath = song.getImagepath();

        ((SongViewHolder) holder).name.setText(name);

        ((SongViewHolder) holder).artist.setText(artist);

        if (!imagepath.equalsIgnoreCase("no_image")) //if the album art has valid  imagepath for this song

            Glide.with(c).load(imagepath)
                    .centerCrop()
                    .into(((SongViewHolder) holder).iv);
        else
            ((SongViewHolder) holder).iv.setImageResource(R.drawable.empty);

        ((SongViewHolder) holder).song = song;
    }
}

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

static class SongViewHolder extends RecyclerView.ViewHolder{

    ImageView iv;
    TextView name, artist;
    CardView songListCard;
    private Context ctx;

    private OnLongPressListener mListener;

    SongViewHolder(View v, Context context) {
        super(v);
        this.ctx = context;
        iv= (ImageView) v.findViewById(R.id.album_art);
        name= (TextView) v.findViewById(R.id.name);
        artist= (TextView) v.findViewById(R.id.artist_mini);
        songListCard = (CardView) v.findViewById(R.id.song_list_card);
    }
}

The recyclerview works fine when there are only 150-200 items but when reaching to 600-700 items , the whole app slows down. Could this be because of the way I have used glide in onBindViewHolder?

Poseidon answered 3/6, 2017 at 19:38 Comment(2)
Could you please share some codes??Josejosee
Added the code in editPoseidon
P
12

Solved the problem by removing the NestedScrollView over the recyclerview. The nestedscrollview was not allowing the recyclerview.addOnScrollListener() to be called because of which I was getting a lag on loading more items. Here is how i implemented the loadOnScroll for RecyclerView-

 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if (!recyclerView.canScrollVertically(1))
                onScrolledToBottom();
        }
    });

private void onScrolledToBottom() {
    if (songMainList.size() < songAllList.size()) {
        int x, y;
        if ((songAllList.size() - songMainList.size()) >= 50) {
            x = songMainList.size();
            y = x + 50;
        } else {
            x = songMainList.size();
            y = x + songAllList.size() - songMainList.size();
        }
        for (int i = x; i < y; i++) {
            songMainList.add(songAllList.get(i));
        }
        songsAdapter.notifyDataSetChanged();
    }

}
Poseidon answered 8/6, 2017 at 7:53 Comment(2)
Seems a more logic solution to the problem. The data set size should not have anything to do with a recycler view lag, because - well the view recycles a handful of views, regardless of the data source size. Unless recycling is turned off for view holders of course, but that should really almost never be done.Springhalt
I had the same problem, and removing NestedScrollView also helped in my case. Thank you!Vday
S
14

Sort answer:

LinearLayoutManager(context).apply { isAutoMeasureEnabled = false }
// or in Java
layoutManager.setAutoMeasureEnabled(false)

UPDATE 2020.08.14

Deprecated RecyclerView.LayoutManager#setAutoMeasureEnabled

This method was deprecated in API level 27.1.0. Implementors of LayoutManager should define whether or not it uses AutoMeasure by overriding isAutoMeasureEnabled()


From the doc of RecyclerView.LayoutManager#setAutoMeasureEnabled() we know :

This method is usually called by the LayoutManager with value {@code true} if it wants to support WRAP_CONTENT

It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based on children's positions.

If we set mAutoMeasure = true, it will call LayoutManager#onLayoutChildren(Recycler, State) during an RecyclerView#onMeasure(int, int) call. Every child view's onMeasure() method will be called, this cost too much time.

enter image description here


Let's look at LinearLayoutManager's constructor

public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
    setOrientation(orientation);
    setReverseLayout(reverseLayout);
    setAutoMeasureEnabled(true);
}

So, after we set mAutoMeasure = false, everything will be ok.

enter image description here

Sympetalous answered 1/8, 2017 at 2:59 Comment(1)
RecyclerView.LayoutManager#setAutoMeasureEnabled() is deprecated now, and need to override #isAutoMeasureEnabled() insteadMagulac
P
12

Solved the problem by removing the NestedScrollView over the recyclerview. The nestedscrollview was not allowing the recyclerview.addOnScrollListener() to be called because of which I was getting a lag on loading more items. Here is how i implemented the loadOnScroll for RecyclerView-

 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if (!recyclerView.canScrollVertically(1))
                onScrolledToBottom();
        }
    });

private void onScrolledToBottom() {
    if (songMainList.size() < songAllList.size()) {
        int x, y;
        if ((songAllList.size() - songMainList.size()) >= 50) {
            x = songMainList.size();
            y = x + 50;
        } else {
            x = songMainList.size();
            y = x + songAllList.size() - songMainList.size();
        }
        for (int i = x; i < y; i++) {
            songMainList.add(songAllList.get(i));
        }
        songsAdapter.notifyDataSetChanged();
    }

}
Poseidon answered 8/6, 2017 at 7:53 Comment(2)
Seems a more logic solution to the problem. The data set size should not have anything to do with a recycler view lag, because - well the view recycles a handful of views, regardless of the data source size. Unless recycling is turned off for view holders of course, but that should really almost never be done.Springhalt
I had the same problem, and removing NestedScrollView also helped in my case. Thank you!Vday
E
3

Do you load the data all at once? RecycleView should not have problem, I think if you have too much data the processing itself can take too much time. You should load the data in chunks and check the scroll state of the user and load the next batch etc. kinda how Instagram or Facebook does it.

Exodus answered 3/6, 2017 at 20:13 Comment(3)
I have added the code and i had implemented the recyclerview in similar way for another recyclerview which is working fine...but i think the problem is with glide reloading the images on notifyDatasetChanged()Poseidon
@Exodus I wish to do the same but I have no idea how to load the chunks of data. Any article for the same?Gaol
Fetching data should never cause a recycler view to lag, otherwise there is an architectural problem and UI tasks are not properly separated from data tasks. Data fetching should happen on background threads only.Springhalt
E
2

you may achieve this by using AndroidX Room & Paging

  1. cache your data from network to local Room Database
  2. using Paging to load your data from Room Database
Ethelred answered 19/1, 2021 at 9:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.