falvojr
falvojr

Reputation: 3080

Android RecyclerView Scrolling Performance

I have created RecyclerView example basing on Creating Lists and Cards guide. My adapter have a pattern implementation only for inflate the layout.

The problem is the poor scrolling performance. This in a RecycleView with only 8 items.

In some tests I verified that in Android L this problem does not occurs. But in the KitKat version the decreasing of performance is evident.

Upvotes: 97

Views: 78366

Answers (16)

khoshrang
khoshrang

Reputation: 156

if you are using images in recycler view try using the function below to make images more lightweight

        fun getImageThumbnail(context: Context, imageFileName: String): Bitmap? {

        val options = BitmapFactory.Options()
        options.inSampleSize = 2
        options.inPreferredConfig = Bitmap.Config.RGB_565;

        val directory = context.filesDir
        val file = File(directory, imageFileName)
        if(!file.exists()) return null
        val bitmap:Bitmap = BitmapFactory.decodeStream(FileInputStream(file),null, options)!!
        var bigger = bitmap.width
        if(bitmap.width<bitmap.height) bigger = bitmap.height
        val zarib = (200.toDouble()/bigger)
        val width = (bitmap.width * zarib).toInt()
        val height = (bitmap.height * zarib).toInt()

        return ThumbnailUtils.extractThumbnail(bitmap,width,height)
    }

Upvotes: 0

Hamza Regardless
Hamza Regardless

Reputation: 83

i Solve this issue by using the only one line in with Picasso library

.fit()

Picasso.get().load(currentItem.getArtist_image())

                    .fit()//this wil auto get the size of image and reduce it 

                    .placeholder(R.drawable.ic_doctor)
                    .into(holder.img_uploaderProfile, new Callback() {
                        @Override
                        public void onSuccess() {


                        }

                        @Override
                        public void onError(Exception e) {
                            Toast.makeText(context, "Something Happend Wrong Uploader Image", Toast.LENGTH_LONG).show();
                        }
                    });

Upvotes: -1

Sami Adam
Sami Adam

Reputation: 86

Adding to @Galya's answer, in bind viewHolder,I was using Html.fromHtml() method. apparently this has performance impact.

Upvotes: 1

eli
eli

Reputation: 9228

I solved it by this line of code

recyclerView.setNestedScrollingEnabled(false);

Upvotes: 4

saintjab
saintjab

Reputation: 1642

Its also important to check the parent layout in which you put in your Recyclerview. I had a similar scrolling issues when I testing recyclerView in a nestedscrollview. A view that scrolls in another view that scroll can suffer in performance during scrolling

Upvotes: 3

Ranjithkumar
Ranjithkumar

Reputation: 18356

In mycase I have complex recyclerview childs. So It affected the activity loading time (~5 sec for activity rendering)

I load the adapter with postDelayed() -> this will give the good result for activity rendering. after activity rendering my recyclerview load with smooth.

Try this answer,

    recyclerView.postDelayed(new Runnable() {
        @Override
        public void run() {
            recyclerView.setAdapter(mAdapter);
        }
    },100); 

Upvotes: 1

blastervla
blastervla

Reputation: 618

In addition to @Galya's detailed answer, I want to state that even though it may be an optimization issue, it is also true that having the debugger enabled can slow things down a lot.

If you do everything to optimize your RecyclerView and it still doesn't work smoothly, try switching your build variant to release, and check how it works in a non-development environment (with the debugger disabled).

It happened to me that my app was performing slowly in debug build variant, but as soon as I switched to the release variant it worked smoothly. This doesn't mean that you should develop with the release build variant, but it is good to know that whenever you are ready to ship your app, it will work just fine.

Upvotes: 11

Sajjad
Sajjad

Reputation: 3218

In my RecyclerView, I use Bitmap Images For background of my item_layout.
Everything @Galya said is true (and I thank him for his great answer). But they didn't work for me.

This is what solved my problem:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);

For more information please read this Answer.

Upvotes: 3

Roar Gr&#248;nmo
Roar Gr&#248;nmo

Reputation: 3956

This helped me getting more smooth scrolling:

override the onFailedToRecycleView(ViewHolder holder) in the adapter

and stop any ongoing animations (if any) holder."animateview".clearAnimation();

remember to return true;

Upvotes: 1

Oleksandr
Oleksandr

Reputation: 6356

I had a talk about RecyclerView's performance. Here are slides in English and recorded video in Russian.

It contains a set of techniques (some of them are already covered by @Darya's answer).

Here is a brief summary:

  • If Adapter items have fixed size then set:
    recyclerView.setHasFixedSize(true);

  • If data entities can be represented by long (hashCode() for instance) then set:
    adapter.hasStableIds(true);
    and implement:
    // YourAdapter.java
    @Override
    public long getItemId(int position) {
    return items.get(position).hashcode(); //id()
    }
    In this case Item.id() would not work, because it would stay the same even if Item's content has changed.
    P.S. This is not necessary if you are using DiffUtil!

  • Use correctly scaled bitmap. Don't reinvent the wheel and use libraries.
    More info how to choose here.

  • Always use the latest version of RecyclerView. For instance, there were huge performance improvements in 25.1.0 - prefetch.
    More info here.

  • Use DiffUtill.
    DiffUtil is a must.
    Official documentation.

  • Simplify your item's layout!
    Tiny library to enrich TextViews - TextViewRichDrawable

See slides for more detailed explanation.

Upvotes: 11

Galya
Galya

Reputation: 6364

I've recently faced the same issue, so this is what I've done with the latest RecyclerView support library:

  1. Replace a complex layout (nested views, RelativeLayout) with the new optimized ConstraintLayout. Activate it in Android Studio: Go to SDK Manager -> SDK Tools tab -> Support Repository -> check ConstraintLayout for Android & Solver for ConstraintLayout. Add to the dependencies:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. If possible, make all elements of the RecyclerView with the same height. And add:

    recyclerView.setHasFixedSize(true);
    
  3. Use the default RecyclerView drawing cache methods and tweak them according to your case. You don't need third party library to do so:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  4. If you use many images, make sure their size and compression are optimal. Scaling images may also affect the performance. There are two sides of the problem - the source image used and the decoded Bitmap. The following example gives you a hint how to decode аn image, downloaded from the web:

    InputStream is = (InputStream) url.getContent();
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    Bitmap image = BitmapFactory.decodeStream(is, null, options);
    

The most important part is specifying inPreferredConfig - it defines how many bytes will be used for each pixel of the image. Keep in mind that this is a preferred option. If the source image has more colors, it will still be decoded with a different config.

  1. Make sure onBindViewHolder() is as cheap as possible. You can set OnClickListener once in onCreateViewHolder() and call through an interface a listener outside of the Adapter, passing the clicked item. This way you don't create extra objects all the time. Also check flags and states, before making any changes to the view here.

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Item item = getItem(getAdapterPosition());
              outsideClickListener.onItemClicked(item);
          }
    });
    
  2. When data gets changed, try to update only the affected items. For example instead of invalidating the whole data set with notifyDataSetChanged(), when adding / loading more items, just use:

    adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
    adapter.notifyItemRemoved(position);
    adapter.notifyItemChanged(position);
    adapter.notifyItemInserted(position);
    
  3. From Android Developer Web Site :

Rely on notifyDataSetChanged() as a last resort.

But if you need to use it, maintain your items with unique ids:

    adapter.setHasStableIds(true);

RecyclerView will attempt to synthesize visible structural change events for adapters that report that they have stable IDs when this method is used. This can help for the purposes of animation and visual object persistence but individual item views will still need to be rebound and relaid out.

Even if you do everything right, chances are that the RecyclerView is still not performing as smoothly as you would like.

Upvotes: 251

I'm not really sure if the usage of setHasStableId flag is going to fix your issue. Based on the information you provide your performance issue could be related to a memory issue. Your application performance in terms of user interface and memory is quite related.

Last week I discovered my app was leaking memory. I discovered this because after 20 minutes using my app I noticed the UI was performing really slow. Closing/opening an activity or scrolling a RecyclerView with a bunch of elements was really slow. After monitoring some of my users in production using http://flowup.io/ I found this:

enter image description here

The frame time was really really high and the frames per second really really low. You can see that some frames needed about 2 seconds to render :S.

Trying to figure it out what was causing this bad frame time/fps I discovered I had a memory issue as you can see here:

enter image description here

Even when the average memory consumption was close to the 15MB at the same time the app was dropping frames.

That's how I discovered the UI issue. I had a memory leak in my app causing a lot of garbage collector events and that's was causing the bad UI performance because the Android VM had to stop my app to collect memory every single frame.

Looking at the code I had a leak inside a custom view because I was not unregistering a listener from the Android Choreographer instance. After releasing the fix, everything became normal :)

If your app is dropping frames due to a memory issue you should review two common errors:

Review if your app is allocating objects inside a method invoked multiple times per second. Even if this allocation can be performed in a different place where your application is becoming slow. An example could be creating new instances of an object inside a onDraw custom view method on onBindViewHolder in your recycler view view holder. Review if your app is registering an instance into the Android SDK but not releasing it. Registering a listener into a bus event could also be possible leak.

Disclaimer: The tool I've been using to monitor my app is under development. I have access to this tool because I'm one of the developers :) If you want access to this tool we will release a beta version soon! You can join in our web site: http://flowup.io/.

If you want to use different tools you can use: traveview, dmtracedump, systrace or the Andorid performance monitor integrated into Android Studio. But remember that this tools will monitor your connected device and not the rest of your user devices or Android OS installations.

Upvotes: 10

SMBiggs
SMBiggs

Reputation: 11688

I discovered at least one pattern that can kill your performance. Remember that onBindViewHolder() is called frequently. So anything you do in that code has the potential to slam your performance to halt. If your RecyclerView does any customization, it's very easy to accidentally put some slow code in this method.

I was changing the background images of each RecyclerView depending on the position. But loading images takes a bit of work, causing my RecyclerView to be sluggish and jerky.

Creating a cache for the images worked wonders; onBindViewHolder() now just modifies a reference to a cached image instead of loading it from scratch. Now the RecyclerView zips along.

I know that not everyone will have this exact problem, so I'm not bothering to load code. But please consider any work that is done in your onBindViewHolder() as a potential bottle-neck for poor RecyclerView performance.

Upvotes: 13

Wei
Wei

Reputation: 1038

In my case, I found out that the notable cause of the lag is frequent drawable loading inside #onBindViewHolder() method. I solved it just by loading the images as Bitmap once inside the ViewHolder and access it from the mentioned method. That is all I did.

Upvotes: 2

Joel
Joel

Reputation: 838

I see in the comments that you are already implementing the ViewHolder pattern, but I will post an example adapter here that uses the RecyclerView.ViewHolder pattern so you can verify that you are integrating it in a similar way, again your constructor can vary depending on your needs, here is an example:

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

    Context mContext;
    List<String> mNames;

    public RecyclerAdapter(Context context, List<String> names) {
        mContext = context;
        mNames = names;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext())
                .inflate(android.R.layout.simple_list_item_1, viewGroup, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        //Populate.
        if (mNames != null) {
            String name = mNames.get(position);

            viewHolder.name.setText(name);
        }
    }

    @Override
    public int getItemCount() {

        if (mNames != null)
            return mNames.size();
        else
            return 0;
    }

    /**
     * Static Class that holds the RecyclerView views. 
     */
    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView name;

        public ViewHolder(View itemView) {
            super(itemView);
            name = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }
}

If you have any trouble working with RecyclerView.ViewHolder make sure you have the appropriate dependencies which you can verify always at Gradle Please

Hope it resolves your problem.

Upvotes: 1

Related Questions