Using NativeScript’s ImageCache to cache images and display them in list like controls

In this article I will review one of the very common usage you encounter when developing a mobile application that shows a list of images (i.e. an image gallery) and how you can use {N}’s ImageCache object to enhance the user experience by caching all images so they are loaded only once.

But let us start with the initial sample code. First we will create a class that we will bind for each image in the list:

import observable = require("data/observable");

export class ImageItem extends observable.Observable
{
    constructor(imageSrc : string)
    {
        super();
        this.set("imageSrc", imageSrc);
    }
}

Then we create a very simple page that will display the images:

<Page xmlns="http://schemas.nativescript.org/tns.xsd" 
      navigatingTo="navigatingTo">
    <ListView items="{{ images }}">
      <ListView.itemTemplate>
        <GridLayout>
          <Image src="{{ imageSrc }}" stretch="aspectFill" height="100"/>
        </GridLayout>
      </ListView.itemTemplate>
    </ListView>
</Page>

For simplicity of the example I use the builtin ListView widget, but in case you are creating a real photo gallery you should better use my GridView widget or the ListView widget from Telerik UI for NativeScript.

In the script for the page we have:

import observableArray = require("data/observable-array");
import observable = require("data/observable");
import imageItem = require("./image-item");
import pages = require("ui/page");

export function navigatingTo(args: pages.NavigatedData)
{
    var page = <pages.Page>args.object;
    var model = new observable.Observable();
    var images = new observableArray.ObservableArray<imageItem.ImageItem>();

    images.push(new imageItem.ImageItem("http://foo.com/bar1.jpg"));
    images.push(new imageItem.ImageItem("http://foo.com/bar2.jpg"));
    // ...
    images.push(new imageItem.ImageItem("http://foo.com/bar100.jpg"));

    model.set("images", images);
    page.bindingContext = model;
}

If we try this sample now you will notice that when you scroll down/up each image is downloaded every time it comes into visible range. And if the images are big this makes the ListView looks unresponsive (not to mention that if your user is using cellular data they wont be very happy :)).

Now we will enhance this by adding ImageCache, so once the image is loaded, subsequent loads will happen from the cache. In order to do so we will change a bit the implementation of the ImageItem class:

import observable = require("data/observable");
import imageCache = require("ui/image-cache");
import imageSource = require("image-source");

/* 1 */
var cache = new imageCache.Cache();
cache.maxRequests = 10;
cache.placeholder = imageSource.fromFile("~/images/no-image.png"));

export class ImageItem extends observable.Observable
{
    private _imageSrc: string
    get imageSrc(): imageSource.ImageSource
    {
        var image = cache.get(this._imageSrc); /* 2 */

        if (image)
        {
            return image; /* 3 */
        }

        cache.push(
            {
                key: this._imageSrc
                , url: this._imageSrc
                , completed:
                (image) =>
                {
                    this.notify(
                        {
                            object: this
                            , eventName: observable.Observable.propertyChangeEvent
                            , propertyName: "imageSrc"
                            , value: image
                        }); /* 4 */
                }
            });

        return cache.placeholder; /* 5 */
    }

    constructor(imageSrc : string)
    {
        super();
        this._imageSrc = imageSrc; /* 6 */
    }
}

With /* 1 */ we create an ImageCache object that is global and will be used for all ImageItems. We set up how many simultaneous downloads we allow and what image to display while the image is being loading. We changed the constructor so that instead of using Observable.set method we introduce a private class member that will contain the URL for the image and we initialize it with /* 6 */. Then we add a property imageSrc that first tries to pull the image from the cache with /* 2 */. If this returns an image then we directly return it with /* 3 */. If an image was not returned then it was not cached and we need to cache it first. For key in the cache we use the URL of the image as it uniquely identifies it. Because the cache loads the image asynchronously, while the image is being loaded we return with /* 5 */ the placeholder image that the user will see until the real image has been downloaded. Once the cache completes loading the image with /* 4 */ we notify all bound widgets that the imageSrc property was changed and they should pull the new data. What this will do is force each Image in the ListView to read the imageSrc property again and this time, because the image has been cached, the real image will be returned with /* 3 */.

By using this approach each image will be downloaded only once and any subsequent loads will not make any network traffic and will be returned instantly from the ImageCache.