RecyclerView
About
The RecyclerView integration library makes the RecyclerViewPreloader
available in your application. RecyclerViewPreloader
can automatically load images just ahead of where a user is scrolling in a RecyclerView.
Combined with the right image size and an effective disk cache strategy, this library can dramatically decrease the number of loading tiles/indicators users see when scrolling through lists of images by ensuring that the images the user is about to reach are already in memory.
Gradle
To use the RecyclerView integration library, add a dependency on it in your build.gradle
file:
compile ("com.github.bumptech.glide:recyclerview-integration:4.8.0") {
// Excludes the support library because it's already included by Glide.
transitive = false
}
If you haven’t already, you will also need to make sure that you already have a dependency on RecyclerView
and that you’re using RecyclerView
in your app :).
Setup
To use the RecyclerView
integration library you need to follow a couple of steps:
- Create a
PreloadSizeProvider
- Create a
PreloadModelProvider
- Create the
RecyclerViewPreloader
given thePreloadSizeProvider
andPreloadModelProvider
s you created in the first two steps - Add your
RecyclerViewPreloader
to yourRecyclerView
as a scroll listener.
Each of these steps is outlined in more detail below.
PreloadSizeProvider
After you add the gradle dependency, you next need to create a PreloadSizeProvider
. The PreloadSizeProvider
is responsible for making sure your RecyclerViewPreloader
loads images in the same size as those loaded by your adapters onBindViewHolder
method.
Glide provides two built in implementations of PreloadSizeProvider
:
If you have uniform View
sizes in your RecyclerView
, you’re loading images with into(ImageView)
and you’re not using override()
to set a different size, you can use ViewPreloadSizeProvider
.
If you’re using override()
or are otherwise loading image sizes that don’t exactly match the size of your Views
, you can use FixedPreloadSizeProvider
.
If the logic required to determine the image size used for a given position in your RecyclerView
doesn’t fit either of those cases, you can always write your own implementation of PreloadSizeProvider
.
If you are using a fixed size to load your images, typically FixedPreloadSizeProvider
is simplest:
private final imageWidthPixels = 1024;
private final imageHeightPixels = 768;
...
PreloadSizeProvider sizeProvider =
new FixedPreloadSizeProvider(imageWidthPixels, imageHeightPixels);
PreloadModelProvider
The next step is to implement your PreloadModelProvider
. The PreloadModelProvider
performs two actions. First it collects and returns a list of Models
(the items you pass in to Glide’s load(Object)
method, like URLs or file paths) for a given position. Second it takes a Model
and produces a Glide RequestBuilder
that will be used to preload the given Model
into memory.
For example, let’s say that we have a RecyclerView
that contains a list of image urls where each position in the RecyclerView
displays a single URL. Then, let’s say that you load your images in your RecyclerView.Adapter
’s onBindViewHolder
method like this:
private List<String> myUrls = ...;
...
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
ImageView imageView = ((MyViewHolder) viewHolder).imageView;
String currentUrl = myUrls.get(position);
GlideApp.with(fragment)
.load(currentUrl)
.override(imageWidthPixels, imageHeightPixels)
.into(imageView);
}
Your PreloadModelProvider
implementation might then look like this:
private List<String> myUrls = ...;
...
private class MyPreloadModelProvider implements PreloadModelProvider {
@Override
@NonNull
List<U> getPreloadItems(int position) {
String url = myUrls.get(position);
if (TextUtils.isEmpty(url)) {
return Collections.emptyList();
}
return Collections.singletonList(url);
}
@Override
@Nullable
RequestBuilder getPreloadRequestBuilder(String url) {
return
GlideApp.with(fragment)
.load(url)
.override(imageWidthPixels, imageHeightPixels);
}
}
It’s critical that the RequestBuilder
returned from getPreloadRequestBuilder
use exactly the same set of options (placeholders, transformations etc) and exactly the same size as the request you start in onBindViewHolder
. If any of the options aren’t exactly the same in the two methods for a given position, your preload request will be wasted because the image it loads will be cached with a cache key that doesn’t match the cache key of the image you load in onBindViewHolder
. If you have trouble getting these cache keys to match, see the debugging page.
If you have nothing to preload for a given position, you can return an empty list from getPreloadItems
. If you later discover that you’re unable to create a RequestBuilder
for a given Model
, you may return null
from getPreloadRequestBuilder
.
RecyclerViewPreloader
Once you have your PreloadSizeProvider
and your PreloadModelProvider
, you’re ready to create your RecyclerViewPreloader
:
private final imageWidthPixels = 1024;
private final imageHeightPixels = 768;
private List<String> myUrls = ...;
...
PreloadSizeProvider sizeProvider =
new FixedPreloadSizeProvider(imageWidthPixels, imageHeightPixels);
PreloadModelProvider modelProvider = new MyPreloadModelProvider();
RecyclerViewPreloader<Photo> preloader =
new RecyclerViewPreloader<>(
Glide.with(this), modelProvider, sizeProvider, 10 /*maxPreload*/);
Using 10 for maxPreload is just a placeholder, for a detailed discussion on how to pick a number, see the section immediately below this one.
maxPreload
The maxPreload
is an integer that indicates how many items you want to preload. The optimal number will vary by your image size, quantity, the layout of your RecyclerView
and in some cases even the devices your application is running on.
A good starting point is to pick a number large enough to include all of the images in two or three rows. Once you’ve picked your initial number, you can try running your application on a couple of devices and tweaking it as necessary to maximize the number of cache hits.
An overly large number will mean you’re preloading too far ahead to be useful. An overly small number will prevent you from loading enough images ahead of time.
RecyclerView
The final step, once you have your RecyclerViewPreloader
is to add it as a scroll listener to your RecyclerView
:
RecyclerView myRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
myRecyclerView.addOnScrollListener(preloader);
Adding the RecyclerViewPreloader
as a scroll listener allows the RecyclerViewPreloader
to automatically load images ahead of the direction the user is scrolling in and detect changes of direction or velocity.
Warning - Glide’s default scroll listener, RecyclerToListViewScrollListener
assumes you’re using a LinearLayoutManager
or a subclass and will crash if that’s not the case. If you’re using a different LayoutManager
type, you will need to implement your own OnScrollListener
, translate the calls RecyclerView
provides into positions, and call RecyclerViewPreloader
with those positions.
All together
Once you’ve completed all of these steps, you’ll end up with something like this:
public final class ImagesFragment extends Fragment {
// These are totally arbitrary, pick sizes that are right for your UI.
private final imageWidthPixels = 1024;
private final imageHeightPixels = 768;
// You will need to populate these urls somewhere...
private List<String> myUrls = ...;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View result = inflater.inflate(R.layout.images_fragment, container, false);
PreloadSizeProvider sizeProvider =
new FixedPreloadSizeProvider(imageWidthPixels, imageHeightPixels);
PreloadModelProvider modelProvider = new MyPreloadModelProvider();
RecyclerViewPreloader<Photo> preloader =
new RecyclerViewPreloader<>(
Glide.with(this), modelProvider, sizeProvider, 10 /*maxPreload*/);
RecyclerView myRecyclerView = (RecyclerView) result.findViewById(R.id.recycler_view);
myRecyclerView.addOnScrollListener(preloader);
// Finish setting up your RecyclerView etc.
myRecylerView.setLayoutManager(...);
myRecyclerView.setAdapter(...);
...
return result;
}
private class MyPreloadModelProvider implements PreloadModelProvider {
@Override
@NonNull
public List<U> getPreloadItems(int position) {
String url = myUrls.get(position);
if (TextUtils.isEmpty(url)) {
return Collections.emptyList();
}
return Collections.singletonList(url);
}
@Override
@Nullable
public RequestBuilder getPreloadRequestBuilder(String url) {
return
GlideApp.with(fragment)
.load(url)
.override(imageWidthPixels, imageHeightPixels);
}
}
}
Examples
Glide’s sample apps contain a couple of example usages of RecyclerViewPreloader
, including:
- FlickrPhotoGrid, uses a
FixedPreloadSizeProvider
to preload in the flickr sample’s two smaller photo grid views. - FlickrPhotoList uses a
ViewPreloadSizeProvider
to preload in the flickr sample’s larger list view. - MainActivity in the Giphy sample uses a
ViewPreloadSizeProvider
to preload GIFs while scrolling. - HorizontalGalleryFragment in the Gallery sample uses a custom
PreloadSizeProvider
to preload local images while scrolling horizontally.
Tips and tricks
- Use
override()
to ensure that the images you’re loading are uniformally sized in yourAdapter
and in yourRecyclerViewPreloader
. You don’t necessarily need to match the size of theView
you’re using exactly with the dimensions you pass in tooverride()
, Android’sImageView
class can easily handle scaling up or down minor differences in sizes. - Fewer larger images are often faster to load than many smaller images. There’s a fair amount of overhead to starting each request, so if you can, load fewer and larger images in your UI.
- If scrolling is janky, consider using
override()
to deliberately decrease image sizes. Uploading textures (Bitmaps) on Android can be expensive, especially for large Bitmaps. You can useoverride()
to force the images to be smaller than yourViews
for smoother scrolling. You can even swap out the lower resolution images with higher resolution images once the user stops scrolling if you’re concerned about quality. - Check out the unexpected cache misses section of the debugging docs page if you’re having trouble getting your
Adapter
to use the images loaded in yourRecyclerViewPreloader