Image Views in Titanium – How to Deal with Caching

**  Update ** for the time being, I’m disabling all of the commenting functionality on my posts because of terrible amounts of spam circumventing my captcha.  If you need to contact me for a question in particular, please reach me at kevin at h-pk-ns.com.  Thanks!

 

So lately I’ve been doing a lot of client work on the mobile side of things, which has been an all new adventure from the start.  Things are much much different in the mobile world.  You have to think a bit differently and solve problems in a way that you almost feel is unclean.

Titanium has been great, I’ve had two apps done and successfully pass review to the App Store and Android Market in about the past two months.  During the first one, a conference specific app, there were a lot of remote images being pulled from a server running Expression Engine.  Basically, the client was entering the data and uploading the photos into EE, which I then used to generate a real-time XML feed that I pumped into the mobile app.

Works great right?  Yeah, until you run into dealing with image URLs.

iOS is a great platform in my 100% honest opinion.  It has it’s idiosyncrasies, but like everything else, you just have to adapt and keep on trudging through it.  In our app, we used Image View objects quite a bit.  Exhibitors had logos, speakers had pictures, social feeds had avatars.  Images are just unavoidable in a rich application these days.  In Titanium, they’ve got their Image View class which handles all the hard Objective-C so I don’t have to, but this class doesn’t do caching of images for offline viewing out of the box.  Well, if the remote URL for the image times out, or doesn’t exist, or there’s a network interruption, etc, it’s okay.  It’s not going to fall over.  It just renders that plain looking mountain picture as the image placeholder until the image is loaded into memory.

Android on the other hand, is a bit wonkier when dealing with remote url images.  The implementation of the images are the same as in iOS with Titanium, but if you don’t have your images on a CDN or local file path, you can forget it.  In my experience, through both the emulator from Google as well as on a device, your application will crash if there is not immediate response with an image.  Full on crashes.  Stupid right?

This is a disturbance.  The objects want their image and they want it to saved locally.  I can 100% understand not having all the assets in the application directory from scratch.  It’s more to manage, changes have to updated, reviewed by Apple, and then downloaded by the users, if you have a lot, that 20MB threshold is right around the corner until your users have to have a Wi-Fi connection to download the app.  These are all critical details when planning and developing a SUCCESSFUL application.  If your app deals with users at a certain location to provide them something they don’t currently have, downloading over 3G is much more spontaneous and rewarding then trying to remember to download the app next time your at a hotspot.

The answer to this is sample, just cache it.  It’s not going to hurt.  Have you looked at the size of some of the applications on your device now?  They are most definitely pulling images and caching them.  Evernote based their whole platform off of saving data.  It is the way of the current webosphere.  Saving content for offline use also makes people much happer, well maybe me atleast.

While working on my applications, I banged out a quick and dirty library called cachedImageView.  Very simple to implement.  Basically, create your imageview object, call the cachedImageView function, pass it the directory you want to save files to, the remote url, and the imageview object itself.  Like so:

function createMyImage() { Ti.include('lib/cachedImageView.js'); var win = Ti.UI.currentWindow; var imgMyImage = Ti.UI.createImageView({ width: 120, height: 120, image: 'default.png' }); cachedImageView('imageDirectoryName', 'http://MyRemoteServer.com/public/images/1.png', imgMyImage); win.add(imgMyImage); win.open({animated:true}); };

 

You can grab this library at Github on my profile khopkins218.

Or from this Gist:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
/*
Developed by Kevin L. Hopkins (http://kevin.h-pk-ns.com)
You may borrow, steal, use this in any way you feel necessary but please
leave attribution to me as the source. If you feel especially grateful,
give me a linkback from your blog, a shoutout @Devneck on Twitter, or
my company profile @ http://wearefound.com.
 
/* Expects parameters of the directory name you wish to save it under, the url of the remote image,
and the Image View Object its being assigned to. */
cachedImageView = function(imageDirectoryName, url, imageViewObject)
{
// Grab the filename
var filename = url.split('/');
filename = filename[filename.length - 1];
// Try and get the file that has been previously cached
var file = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, imageDirectoryName, filename);
 
if (file.exists()) {
// If it has been cached, assign the local asset path to the image view object.
imageViewObject.image = file.nativePath;
} else {
// If it hasn't been cached, grab the directory it will be stored in.
var g = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, imageDirectoryName);
if (!g.exists()) {
// If the directory doesn't exist, make it
g.createDirectory();
};
 
// Create the HTTP client to download the asset.
var xhr = Ti.Network.createHTTPClient();
 
xhr.onload = function() {
if (xhr.status == 200) {
// On successful load, take that image file we tried to grab before and
// save the remote image data to it.
file.write(xhr.responseData);
// Assign the local asset path to the image view object.
imageViewObject.image = file.nativePath;
};
};
 
// Issuing a GET request to the remote URL
xhr.open('GET', url);
// Finally, sending the request out.
xhr.send();
};
};
view raw cachedImageView.js hosted with ❤ by GitHub

More robust documentation is in the code.  Hope this eases the pain!

No related posts.

About Kevin L. Hopkins

Software developer (web, mobile, and desktop) located in Harrisonburg, VA. Employed as lead developer and systems administrator at Found Design + Interactive. Language polyglot, but focus on Ruby, Javascript, Actionscript 3.0 and C# using different frameworks for different tasks. View all posts by Kevin L. Hopkins

27 Responses to “Image Views in Titanium – How to Deal with Caching”

  • Arun

    How to get the image Directory ?

    I have a folder Images in the Resources.

    How can I get that directory ?

    Do this suit to save audio file in local ?

  • Kevin L. Hopkins

    The image directory that you have in your Resources directory is only for application included files such as images for UI objects, backgrounds, menus, buttons, etc. Once your app is on a device, the resources directory is no longer writeable and the application code is read only. When Titanium compiles down your code to native Obj-C, the file hierarchy is totally changed and is no longer how it appears pre-compilation. This is why we have to use the Ti.Filesystem.applicationDataDirectory which is writeable and persistent. You can access the Resources directory by using Ti.Filesystem.resoucesDirectory, but only for the purpose of accessing and reading files such as non-dynamic XML files.  It’s also useful for including other files in your Ti.include()’s for being sure you have the application’s root directory. If you want the data to only be temporarily stored and cleaned up when the application is fully closed, you can use the Ti.Filesystem.tempDirectory. During compilation, these pieces of code are compiled to Obj-C pointers to the new directory locations.

    In the code above, you can name this directory however you want by passing to the function the directory name that you want to use as a file bucket. As for audio files,  you can use similar functionality to download the file from a remote source, but this function is built to take the images, cache them on the device, and assign the native file path as the image source to the imageView object passed in, and to return that object back to the original scope. 

  • Jonathan Eatherly

    Wow, thanks so much! I was just about to write this myself since the default remote images in Titanium are a joke. I know they have good intentions but the lazy load was killing my app’s performance. I also noticed that the scrolling performance is greatly improved for images in a scrollView.

    The only issue I have is that using the cachedImageView seems to overwrite the defaultImage property of the imageView. I am going to try modifying your code to accept a defaultImage property and set the image to that while the remote resource is being loaded.

    Thanks again!

  • Kevin L. Hopkins

    Hey Jonathan thanks for the great feedback. I probably should modify it for defaultImages :( . That was something I didn’t think about as I wrote it. the project I used it on had some local images that changed in an upper model view as you selected individuals from a tableview below in a separate modal.

    If you need help with getting that portion rolling, don’t hesitate to give me a shout!

    Thanks again!

  • Rodrigo

    Hello Kevin, I’m trying to use this technique but I’m having problems when the user returns the navigation before loading all the images an error occurs, think it is because the ImageView was destroyed and the code attempts to assign a value to the property variable that is not another object.
    How to solve this? Thanks.

  • Kevin L. Hopkins

    Hey Rodrigo,
    You could probably get by with using either a try/catch block, but that still may error out depending on how fast the process is actually running and how long it takes the server to respond with the image.

    Probably what I would do personally is right before the line “imageViewObject.image = file.nativePath;”, I would do a “if (imageViewObject) { imageViewObject.image = file.nativePath; };

    This would kind of nip it in the bud just in case the obj really is null, as in the case you mentioned. I think your causation to the problem is totally correct. nice catch! I’ll be incorporating this in my update to the code I’ve been slowly working on.

    Hope this helps and if not, feel free to give me a yell!

    ~Kevin

  • kon

    Thanks for this class, I just tried to use it and ran into the following problem:
    It works fine for the first time, that is until i restart the app or send it to the background. After that the images appear empty, i.e. transparent.
    file.exists returns true and also the filename seems to be correct. I am stumped… do you maybe have any idea what could be the problem?

  • Kevin L. Hopkins

    Sorry it took me so long to get back to you Kon. I’ve tried to replicate this using the emulator and was able to make it have issues like you described but I can’t replicate it on the device itself. Which platform (emulator or device) are you using and what iOS version number so I can see if I can dig a little deeper.

    Overall, I’ve had issues with apps running in the background and then coming back foreground in general. Tyipcally, I just save to Ti.App what the last window was before being backgrounded, and on refocus I redraw that entire page.

    Another quick question, have you had the same issue when not using the imagecaching function? I would try it without it and if it then works flawlessly, we’ll know that theres a bug somewhere.

    Good Luck
    ~Kevin

  • kon

    Hey Kevin

    Thanks for the reply and no worries.
    When I was testing the image caching I was using the emulator (iOS SDK 4.2, Titanium SDK 1.6.2) and thus didn’t test it on an actual device after it didn’t work on the emulator.
    So, does this actually mean that if something on the emulator works or doesn’t for that matter, then it doesn’t necessarily mean it will behave the same on a device?

    To the problem:
    It worked fine on the emulator as long as I didn’t use cached image views, and as described it even worked for the first time the app was launched when using cached image views in the emulator after the device contents had been reset via the emulator’s menu, just not for consecutive launches or when the app was brought back from the background.

    I’d be happy to provide more information if you need any.

    Cheers!

  • Kevin L. Hopkins

    Your emulator, sdk, and titanium versions all check out with what’s working for me.

    I’ve come across numerous issues with things that don’t work at all or very well on the emulator but work fine on the device itself, mostly dealing with GPS and location based logic, as well as database and file storage on the emulator (granted most of these issues were with 4.0 and earlier). The vast majority of the rest of the functionality based on animations, layouts, window views, table views, etc. are all pretty solid between the emulator and the device.

    The one caveat for this is the navigation history. If you use the default back buttons when following window or view navigation, on the emulator when going backgrounded and coming back sometimes that gets pretty nuts. It often loses its history and theres no way to get back to where you need to be if you hide the tab bar and don’t have close buttons.

    If I were in your situation, I’d try it on a device (if you have one) and see if it works there. Otherwise, if you want to share your code with me (khopkins218 on github and codaset) or at kevin at h-pk-ns dot com I’d be more than happy to see if I can’t troubleshoot it some more.

    Good luck!

  • Mike Heavers

    Hey – I’m trying to pull in an XML feed into appcelerator too (super new at Appcelerator) – if I load the url and try to get a response, nothing happens.

    My feed URL is: http://superfad.com/work/rss

    And I’m trying this:


    function loadProjects()
    {
    Ti.API.info('loading')
    var rowData = [];
    var loader = Titanium.Network.createHTTPClient();
    loader.open("GET","http://superfad.com/work/rss");
    loader.onload = function(){
    Ti.API.info('loaded')
    var projects = eval('('+this.responseText+')');
    Titanium.API.info('Received: '+projects);
    }
    }

    loadProjects();

    But the onload never triggers. What method did you use to load the XML. Is my Feed improperly formatted?

  • Kevin L. Hopkins

    Hey Mike,

    Your code looks good and correct minus the actual command to send out the request.


    function loadProjects()
    {
    Ti.API.info('loading')
    var rowData = [];
    var loader = Titanium.Network.createHTTPClient();
    loader.onload = function(){
    Ti.API.info('loaded')
    var projects = eval('('+this.responseText+')');
    Titanium.API.info('Received: '+projects);
    }
    loader.open("GET","http://superfad.com/work/rss");
    loader.send();
    }
    loadProjects();

    Check right after the onload function, I moved the loader.open with the request type and url there (it can be anywhere, just personal preference) but right after that, you need loader.send(); which actually executes the request.

    Otherwise, you should be clean and clear in terms of execution, I must give you the standard warning about the “eval” though. If there is anything malicious or debilitating in the RSS (other code bits) they may get evaluated and run amok through your application.

    I think your comment gives me a good next topic as it’s been a bit since I’ve posted. I’ve got a great example of loading in XML data, iterating over it, and using it for data population in the app.

    Hope this helps!

  • Quora

    Is it possible to preload URL-based images?…

    The general practice is to create a custom wrapper around a ImageView that downloads the image as you create it and add it to the TableRow. I’d recommend caching the image – an ImageView will show almost instantly for local files. See http://kevin.h-p

  • Kristian

    You saved my day. Thank you.

  • ads

    Hi Kevin,
    in this case how do you manage retina images ?
    Regards

  • Kevin L. Hopkins

    @Kristian — you are more than welcome

    @ads Why do you want to cache retina images? If they’re local to the device, you don’t need to do anything with them, they’re already in filestore and you need to use Ti.UI.Media to get access to them via the photoGallery. If they’re retina images that you save offline somewhere else that you want to load in, you would just use it as it since they’re images like anything else.

    ~Kevin

  • ads

    Kevin,
    I was refering of online image , for instance in a imageview for non retina you will have to download myicon.png but in retina display, display will be better if myicon@2x.png is available. So in retina it will be better to download the @2x image instead of the classic file.
    Regards

  • Kevin L. Hopkins

    Now I understand a bit better, thanks. I believe I read somewhere that the @2x only works for the splash screen and app icon. Within the actual application you’ll have to do platform detection and then append the image path accordingly for how your files are named. Have you tried implementing an image with the @2x in the name without using the cachedImageView function? I think I remember having emulator issues back around Titanium 1.4.

    After some quick research, someone appears to have forked (or copied, cant tell) my logic and expanded on it for Retina support here : https://gist.github.com/978559

  • ads

    Kevin,
    retina image are well supported now, not only App icon and splashscreen. each of my local images are provided in normal and @2x version. Nothing special to do, it is automatically supported. O course for offline image it ios better to detect id it is a retina display and download only the @2x image.

  • Kevin L. Hopkins

    Hey Daniel! Couple of things to help me understand your issue a bit better. Are you using tableviewrows for your rowdata? Are you pushing your data objects into an array and using those array elements later to populate the data in the table? Take for example the following: (This is how I usually do dynamic tableviews)

    ***********************
    var tableView = Ti.UI.createTableView({ backgroundColor:”transparent”, height:”90%”, top:2 });
    var userData = [];

    for (var i = 0; i < app.users.length; i++) {
    var tableViewRow = Ti.UI.createTableViewRow({
    height:50,
    top:4,
    bottom:4,
    hasDetail:true,
    });

    var userImage = Titanium.UI.createImageView({
    height:40,
    width:40,
    left:2
    });

    cachedImageView(‘profile_images’, (app.users[i].user.photo_url), userImage);

    var lblText = Ti.UI.createLabel({
    text:app.users[i].user.login,
    font:{fontSize:14},
    color:”#000″,
    width: 200,
    left:50, top:2,
    hasDetail:true,
    className:”userProfiles”
    });

    tableViewRow.add(userImage);
    tableViewRow.add(lblText);
    userData.push(tableViewRow);
    }

    tableView.data = [];
    tableView.setData(userData);

    ********************

    Basically long story short is this, Ill create my tableView object and an empty array outside my loop that I use to construct my data into view elements. On each iteration, I create a tableViewRow object and add to it my UI elements (an image for users and a label with their username on the app in this example). When I’ve got the tableViewRow all built out the way I want it to appear in the table, I push it into the array I created. After the looping is over and my array is populated entirely, I set the tableView.data to an empty array in the case of say, a refresh of the table and then use tableView.setData(myArray) to set the table data. This works for me all the time, I keep re-using this basic structure over and over.

    Also, of note, I had a recent pull request from a user who added in a nice file = null bit into the function on GitHub, it helps severely with memory management issues. If you’re creating dynamic tables, and caching images for it, I highly recommend you add in this snippet.

    Hope this helps, let me know how your work turns out and if this helped you get on track.

    ~Kevin

  • Daniel

    Thanks for the fast and great reply, ive tested the way you posted but it just doesnt show the tablelist.

    Heres the final code: http://pastebin.com/SHJcA6xF

    Hopefully you have the time to take a look and can spot if im doing something wrong.

    Best regards
    Daniel

  • Tutorial: How to create custom map annotations with Appcelerator | Adam Zwakk

    [...] (outside the Resources folder) and use Kevin Hopkin‘s awesome cachedImageView function (you can get it here): var thisImage = Ti.UI.createImageView({ width:45, height:45 }); [...]

  • jayesh

    is it work woth images on android with big image data ?

  • Tejpratp

    Hi all
    i am using this function working loading images fine
    but if images having more than 5 then while loading
    table view my app is crashing pls help me.
    Thanks

  • Ericka

    It’s hard to find your blog in google. I found it on 17 spot, you should build quality backlinks
    , it will help you to rank to google top 10. I know how to help
    you, just search in google – k2 seo tricks

Leave a Reply

Your email address will not be published. Required fields are marked *

*


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

SEO Powered by Platinum SEO from Techblissonline

Kevin's Blog is Stephen Fry proof thanks to caching by WP Super Cache