Challenge: Prealoading and Caching - Partial Solution

So I was able to solve the first part of the challenge, which was to use LruCache to cache the bitmaps when downloaded. This page from the Android Developers site was very helpful. Here is my solution for that part of the challenge. All code is done in ThumbnailDownloader.java:

First I added a private global variable for the LruCache:

private LruCache<String, Bitmap> mLruCache;

I then added these lines to the constructor for ThumbnailDownloader in order to to initialize mLruCache:

public ThumbnailDownloader(Handler responseHandler) {
    super(TAG);
    mResponseHandler = responseHandler;

    ***added***
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    final int cacheSize = maxMemory / 8;

    mLruCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            return bitmap.getByteCount() / 1024;
        }
    };
    *********

}

Next, I added these methods to the end of ThumbnailDownloader.java:

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mLruCache.put(key, bitmap);
    }
}
public Bitmap getBitmapFromMemCache(String key) {
    return mLruCache.get(key);
}

Lastly, I edited the handleRequest method to add the downloaded bitmaps to the cache, and to check the cache first for the images. If the image is not in the cache, it will download the image. Otherwise, it will take the image from the cache. I used the url for the image as the key:

private void handleRequest(final T target) {
    try {
        final String url = mRequestMap.get(target);

        if(url == null) {
            return;
        }

        ***edited code***
        final Bitmap bitmapFromMemCache = getBitmapFromMemCache(url);
        if (bitmapFromMemCache != null) {
            Log.i(TAG, "Bitmap found in cache");

            mResponseHandler.post(new Runnable() {
                @Override
                public void run() {
                    if(mRequestMap.get(target) != url || mHasQuit) {
                        return;
                    }
                    mRequestMap.remove(target);
                    mThumbnailDownloadListener.onThumbnailDownloaded(target, bitmapFromMemCache);
                }
            });
        } else {
            byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url);
            final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
            Log.i(TAG, "Bitmap created");

            mResponseHandler.post(new Runnable() {
                @Override
                public void run() {
                    if(mRequestMap.get(target) != url || mHasQuit) {
                        return;
                    }
                    mRequestMap.remove(target);
                    addBitmapToMemoryCache(url, bitmap);
                    mThumbnailDownloadListener.onThumbnailDownloaded(target, bitmap);
                }
            });
        }
        ****************
    } catch (IOException ioe) {
        Log.e(TAG, "Error downloading image", ioe);
    }
}

This worked well for me for the first part of the challenge. However, I couldn’t quite figure out how to go about doing the preloading part of it. I tried implementing this into the onBindViewHolder of PhotoAdapter in PhotoGalleryFragment.java, but it didn’t work properly, repeated images at the end, and sometimes caused the app to crash:

        if((mGalleryItems.size() - (position++)) < 10) {
            for(int i = position; i > (position - 10); i--) {
                GalleryItem galleryItem = mGalleryItems.get(i);
                Drawable placeholder = getResources().getDrawable(R.drawable.ic_photo_grey_48dp);
                holder.bindDrawable(placeholder);
                mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
            }
            for(int i = position; i < mGalleryItems.size(); i++) {
                GalleryItem galleryItem = mGalleryItems.get(i);
                Drawable placeholder = getResources().getDrawable(R.drawable.ic_photo_grey_48dp);
                holder.bindDrawable(placeholder);
                mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
            }
        } else if(position < 10) {
            for(int i = position; i >= 0; i--) {
                GalleryItem galleryItem = mGalleryItems.get(i);
                Drawable placeholder = getResources().getDrawable(R.drawable.ic_photo_grey_48dp);
                holder.bindDrawable(placeholder);
                mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
            }
            for(int i = position; i < (position + 10); i++) {
                GalleryItem galleryItem = mGalleryItems.get(i);
                Drawable placeholder = getResources().getDrawable(R.drawable.ic_photo_grey_48dp);
                holder.bindDrawable(placeholder);
                mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
            }
        } else {
            for(int i = position; i > (position - 10); i--) {
                GalleryItem galleryItem = mGalleryItems.get(i);
                Drawable placeholder = getResources().getDrawable(R.drawable.ic_photo_grey_48dp);
                holder.bindDrawable(placeholder);
                mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
            }
            for(int i = position; i < (position + 10); i++) {
                GalleryItem galleryItem = mGalleryItems.get(i);
                Drawable placeholder = getResources().getDrawable(R.drawable.ic_photo_grey_48dp);
                holder.bindDrawable(placeholder);
                mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
            }
        }

I understand this is probably inefficient, but it’s what i could come up with on short notice. Any help would be greatly appreciated. @cstewart, any tips on where I should start?

1 Like

For the cache, you can further improve what you have by checking for cache hits on the main thread. As your code is now, you are always posting a request for an image to the background thread. The background thread can only do one thing at a time, so an existing download for some other image could be ongoing when you request an image that’s already in the cache. You can make the cached image return instantly by updating the code so that you check for a cache hit on the main thread and if there is no image, then post to the background thread.

As for the prefetch, I’d recommend using the onScrollListener on the RecyclerView. There is a callback that can tell you when the scroll state has changed. From that, you can determine when the user has just stopped scrolling. At that time, you can pre-fetch a few of the previous and next rows.

1 Like