Ok, I know we’ve been going on about custom cells / cell factories a bit recently, but I wanted to do one more post about a very useful topic: caching within cell content.
These days ‘Hello World’ has been replaced by building a Twitter client, so I’ve decided to frame this topic in terms of building a Twitter client. Because I don’t actually care about the whole web service side of thing, I’ve neglected to implement the whole ‘real data’ / web services aspect of it. If you want to see an actual running implementation with real data, have a look at William Antônio’s Twitter client, which is using this ListCell implementation.
So, in all the posts to this site related to cells, I’m sure you’ve probably come to appreciate the ways in which you should create a ListView or TreeView with custom cell factories. Therefore, what I really want to cover in this post is just the custom cell implementation, and the importance of caching. A Twitter client wouldn’t be a true client without showing the users profile image, so this is my target for caching. Without caching, each time the cell was updated (i.e. the content changes due to scrolling, or when we scroll a user out of screen and then back in), we’d have to redownload and load the image. This would lead to considerable lag and a poor user experience. What we need to do is load the image once, cache it, and reuse it whenever the image URL is requested by a cell. At the same time, we don’t want to run the PC dry of memory by loading all profile images into memory. Enter: SoftReference caching.
Word of warning: I’m not a caching expert. It is possible that I’ve done something stupid, and I hope you’ll let me know, but I believe that the code below should at least be decent. I’ll happily update this example if anyone gives me useful feedback.
Check out the code below, and I’ll continue to discuss it afterwards.
[jfx]
import model.Tweet;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.util.Math;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Container;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
// controls whether the cache is used or not. This _really_ shouldn’t be false!
def useCache = true;
// map of String -> SoftReference (of Image)
def map = new HashMap();
def IMAGE_SIZE = 48;
public class TwitterListCell extends ListCell {
// used to represent the users image
var imageView:ImageView;
// a slightly bigger and bolder label for the persons name
var personName:Label = Label {
font: Font.font("Arial", FontWeight.BOLD, 13);
}
// the message label
var message:Label = Label {
textWrap: true
}
override var node = Container {
content: bind [ imageView, personName, message ]
override function getPrefHeight(width:Number):Number {
def w = listView.width;
Math.max(IMAGE_SIZE, personName.getPrefHeight(w) + message.getPrefHeight(w));
}
override function doLayout():Void {
var x:Number = -1.5;
var y:Number = 0;
var listWidth = listView.width;
var cellHeight = height;
// position image
Container.positionNode(imageView, x, y, IMAGE_SIZE, cellHeight,
HPos.CENTER, VPos.TOP, false);
// position text at the same indent position regardless of whether
// an image exists or not
x += IMAGE_SIZE + 5;
var textWidth = listWidth – x;
var personNameHeight = personName.getPrefHeight(textWidth);
Container.resizeNode(personName, textWidth, personNameHeight);
Container.positionNode(personName, x, y, listWidth – x, personNameHeight,
HPos.LEFT, VPos.TOP, false);
y += personNameHeight;
Container.resizeNode(message, textWidth, message.getPrefHeight(textWidth));
Container.positionNode(message, x, y, listWidth – x, height – personNameHeight,
HPos.LEFT, VPos.TOP, false);
}
}
override var onUpdate = function():Void {
var tweet = item as Tweet;
personName.text = tweet.person.name;
message.text = tweet.message;
// image handling
if (map.containsKey(tweet.person.image)) {
// the image has possibly been cached, so lets try to get it
var softRef = map.get(tweet.person.image) as SoftReference;
// get the image out of the SoftReference wrapper
var image = softRef.get() as Image;
// check if it is null – which would be the case if the image had
// been removed by the garbage collector
if (image == null) {
// we need to reload the image
loadImage(tweet.person.image);
} else {
// the image is available, so we can reuse it without the
// burden of having to download and reload it into memory.
imageView = ImageView {
image: image;
}
}
} else {
// the image is not cached, so lets load it
loadImage(tweet.person.image);
}
};
function loadImage(url:String) {
// create the image and imageview
var image = Image {
url: url
height: IMAGE_SIZE
preserveRatio: true
backgroundLoading: true
}
imageView = ImageView {
image: image;
}
if (useCache) {
// put into cache using a SoftReference
var softRef = new SoftReference(image);
map.put(url, softRef);
} else {
map.remove(url);
}
}
}
[/jfx]
You’ll note that in this example most of the code is pretty standard. A few variables are created for the image and text, and then I’ve gone the route of laying the content out in a Container, but you can achieve a similar layout using the available layout containers. Following this I have defined an onUpdate function, which is called whenever the cell should be updated. This is usually called due to a user interaction, which may potentially change the Cell.item value, which would of course require an update of the cell’s visuals.
The bulk, and most important part, of the onUpdate function deals with loading the users profile image, or retrieving and reusing the cached version of it. Note the use of the global HashMap, which maps between the URL of the users image and the Image itself. Because it is global (i.e. static), this map will be available, and used, by all TwitterListCell instances. Also important to note is that I didn’t put the ImageView itself into the HashMap as a Node can not be placed in multiple positions in the scenegraph, but an Image can be.
The rest of the code in this class really just deals with the fact that a SoftReference may clear out it’s reference to the Image object if the garbage collector needs the memory, in which case we need to reload the image again. The other obvious part is the need to also put the image into the cache if it’s not already there.
Shown below is the end result, but remember that there is a working version of this demo in William Antônio’s Twitter client, which is a very early work in progress.
I hope this might be useful to people, and as always we’re keen to hear your thoughts and feedback, and what you’re hoping us to cover. Until next time – cheers! 🙂