Implementation

For information about the design and functionality of the MIDlet, see section Design.

The application is implemented using LCDUI and the Tantalum 5 library. As a user interface library, LCDUI provides a basis for creating customized list elements and handling touch events. In the following sections we go through the implementation by taking a look at views, usage of Tantalum 5 and custom data models.

Views

By implementing the ViewManager interface in the main MIDlet, we pass the view manager instance to those parts of the application that have a need to open new views. These parts are mostly other views. In addition, the ViewManager interface contains special methods for handling views with a category bar.

All views and elements in the user interface extend LCDUI's CustomItem, List or Form. List provides a very basic approach for creating simple lists whereas Form based views are used as containers for CustomItem based elements. Views are restricted to portrait mode only.

Grid of thumbnails

Some views implement a grid layout for displaying thumbnail images. In the application, this is achieved by providing an abstract class called AlbumGridView for such views that need to display images in a grid. The AlbumGridView itself extends LCDUI's Form.

public class NewReleasesView
        extends AlbumGridView {
    ...
}

public abstract class AlbumGridView
        extends Form
        implements
            CommandListener,
            ItemStateListener,
            ItemCommandListener {
    // Contains logic for instantiating a GridLayout and reacting to press events.
}

public class GridLayout
        extends CustomItem {
    // Extends a CustomItem and implements painting the grid that consists of GridItems.
}

public class GridItem 
        extends CustomItem {
    // Holds the actual image and is responsible for painting itself.
}

Usage of the ListItemView class

In addition to the grid of thumbnails, there is a ListItem class that is used for a couple of purposes. The ListItem can be constructed using any GenericProductModel to display a thumbnail that is associated to the model and a title. In order to provide special functionality regarding the type of the model, we detect the model type while painting the ListItem. Based on this we know what text to show in titles.

public abstract class ListItemView 
        extends Form
        implements ItemCommandListener {
    
    protected QueryPager queryPager;
    protected LoadMoreButton loadMoreButton;
    protected ViewManager viewManager;
    
    public ListItemView(ViewManager viewManager, String title) {
        super(title);
        this.viewManager = viewManager;
        this.queryPager = new QueryPager();
        this.loadMoreButton = new LoadMoreButton(this);
        this.viewManager = viewManager;
    }

    protected abstract void loadDataset();
    protected abstract void loadNextDataset();
    protected abstract void parseAndAppendToView(JSONObject response) throws JSONException;
    
    public void commandAction(Command c, Item item) {
        if (c == loadMoreButton.getCommand()) {
            loadNextDataset();
        }
    }
    
    public class PlaceResultsTask 
            extends Task {
        ...
    }
}

Category bar in the Artist view

The category bar is used for displaying an info page for an artist that lists the artist's albums in a grid layout. The category bar allows switching to a view that displays artists similar to the current artist. The challenge in implementing a view with a category bar comes from the fact that in this design the category bar view is launched from another view and from the category bar view the user can navigate to other views, too.

The ArtistView is different from other views. The view itself does not display anything, but instead is used for managing the sub-views launched from the tabs of a category bar. In the constructor of the view we can see that the constructor instantiates an ArtistInfoView and then displays it. The ArtistView opens by default always an ArtistInfoView, which is also the first, and default, option of the category bar.

/**
 * Constructs the view based on an artist id.
 * @param viewManager
 * @param artistId 
 */
public ArtistView(ViewManager viewManager, int artistId) {
    this(viewManager);
    this.artistInfoView = new ArtistInfoView(viewManager, artistId, this);
    displayCategoryBar();
    showArtistInfoView();
}

The mechanism how the artist view and its sub-views are handled is vital to understand in order to fully follow the implementation of the application. As a category bar after its instantiation floats on top of everything, it is not dependent on where it was opened at.

At first, lets take a look how an ArtistView is instantiated. In the following snippet taken from AlbumView, the artist view is instantiated and then added to the stack in ViewManager. As we normally use ViewManager's showView() method to display views, the ArtistView instance is here just added to the stack but not displayed.

// Snippet taken from AlbumView.

/**
 * Initialize an ArtistView by performer ID. The view is basically
 * just a blank Form that displays a SimilarArtistsView and 
 * a ArtistInfoView based on the current category bar selection.
 */
ArtistView artistView = new ArtistView(viewManager, albumModel.getPerformerId());

/**
 * Add the view to stack but do not display it. An ArtistView takes 
 * care of displaying the category bar items / sub-views.
 */
viewManager.addToStack(artistView);

By looking into the constructor of ArtistView class, we can see that it automatically begins to instantiate an ArtistInfoView as described above. ArtistView itself manages communication with the ViewManager instance. In this case, the ArtistInfoView is shown using the method showArtistInfoView() that uses ViewManager's method showSubView().

// Snippet taken from ArtistView.

private void showArtistInfoView() {
    lastViewedTab = artistInfoView;
    viewManager.showSubview(artistInfoView);
}

The implementation of the showSubView() method allows having one sub-view either displayed or waiting for the user to step back to it. This is achieved by storing the sub-view in a separate store to the view stack. In addition to detect whether the previous view was a sub-view, we need to check that in the goBack() method of ViewManager.

Tantalum, caching and API calls

As with all ready-made libraries, developers can save a lot of time by using such. In this case, Tantalum abstracts low-level networking. Because of this, developers do not have to deal with the HTTP protocol but instead have more time to concentrate on developing the actual application logic.

Tantalum implements a similar design that is often seen in modern JavaScript web applications. In a nutshell, Tantalum makes HTTP requests asynchronously. Asynchronous calls take a callback object as a parameter that is run when the HTTP response has returned data. In the case of Tantalum, the callback object is an instance of the Task class that is a part of Tantalum. The Task class is extended to provide a custom handler for the expected HTTP response. A custom handler implements a method that is executed to, for example, parse a JSON response to such form that can be consumed by the application.

// Example implementation taken from AlbumGridView

/**
 * Callback task for parsing the JSON response for the view.
 */
protected class PlaceResultsTask extends Task {

    public PlaceResultsTask() {
        super(Task.NORMAL_PRIORITY);
    }

    public Object exec(Object response) {
        if (response instanceof JSONObject) {
            parseJSONForView((JSONObject) response);
        }
        return response;
    }
}

/**
 * Parses a JSONObject for use in grid view.
 *
 * @param model
 */
protected void parseJSONForView(JSONObject model) {
    try {
        addItemsToGrid(model.getJSONArray("items"));
        this.queryPager.setPaging(model.getJSONObject("paging"));
        notifyTotalUpdated();
        
        loadMoreButton.remove();

        if (queryPager.hasMorePages()) {
            loadMoreButton.append();
        }
    } catch (JSONException e) {
        L.e("Error while parsing items to JSON.", "", e);
    }
}

One of the reasons for using Tantalum is that it implements caching in a convenient way. In the application, Tantalum's caching features are used for storing copies of images. By caching, such views that have common cached content can be loaded faster as images can be fetched directly from a cache.

The ApiCache class provides an interface for making requests via caches. In the application, there are two caches that are instances of the Tantalum's StaticWebCache class: a cache for API calls and a cache for images. Only the image cache utilizes caching and stores fetched images locally. The reason for not to store JSON responses is that the content is more dynamic and may change. Caching search results would hide any new results for the same query.

Caching is controlled by constants that reside in the StaticWebCache class. The following code snippet demonstrates how an API call is made to get popular releases. Notice the StaticWebCache.GET_WEB which controls the behaviour of the getAsync() method.

/**
 * Gets popular releases.
 *
 * @param callback The Task to call when an HTTP response is received.
 * @return
 */
public static Task getPopularReleases(Task callback, String pagingQueryString) {
    ApiRequestTask apiRequestTask = new ApiRequestTask(
            viewManager,
            apiCache,
            ApiEndpoint.getChartsResourceUrl(pagingQueryString),
            Task.NORMAL_PRIORITY,
            StaticWebCache.GET_WEB,
            callback);
    
    return apiRequestTask;
}

To demonstrate a different behaviour, the following example shows how to enable caching. In this case, the constant StaticWebCache.GET_ANYWHERE tells getAsync() to fetch the response from either a cache or from the web.

/**
 * Gets an image file from either the Internet or from the Image cache.
 *
 * @param imageUri The resource identifier for the image.
 * @param callback The Task to call when a response has been found.
 * @return
 */
public static Task getImage(String imageUri, Task callback) {
    if (imageUri == null || imageUri.length() == 0) {
        L.i("Invalid image URI.", "");
        return null;
    }
    
    ApiRequestTask apiRequestTask = new ApiRequestTask(
            viewManager,
            imageCache,
            imageUri,
            Task.NORMAL_PRIORITY,
            StaticWebCache.GET_ANYWHERE,
            callback);
    
    return apiRequestTask;
}

Note that all requests are made using an ApiRequestTask instance. The ApiRequestTask class allows performing operations before making the actual request. In this case, such operations are checking for network connection availability. Whenever a connection is not available, an alert is displayed to inform the user. ApiRequestTask performs a test request always before making the actual request. Based on the success of the test request, network connection availability is determined.

Painting thumbnails asynchronously

The GridItem class and the ListItem class are responsible for loading the thumbnail image that is associated to the given model. As described above, the asynchronous behaviour is implemented using Tantalum's Tasks. In this case, image loading is triggered in the paint() method of each grid and list item if the image is not yet loaded.

protected void paint(
        Graphics graphics,
        int x,
        int y,
        int width,
        int height) {

    
    // Check if thumbnail is already fetched. If not, get it from the web.
    if (thumbnail != null) {
        graphics.drawImage(thumbnail, x, y, Graphics.TOP | Graphics.LEFT);
    } else {
        // Paint to the center of the item.
        Placeholders.paint(graphics, model, x, y, width, height);
        getImage();
    }
    
    if(highlight) {
        paintHighlight(graphics, x, y, width, height);
    }
}

Data model

The example application has custom data model classes for storing the actual data that is consumed by the views. Tracks, albums and artists extend the GenericProductModel class.

Query paging

As the Nokia Music REST API can return paged queries the application needs a mechanism for handling such. For this case the QueryPager class was implemented. A QueryPager instance keeps track of the API responses and the paging info they contain. The instance returns a query string that can be appended to REST API calls.

The following code sample is a snippet taken from ArtistsListView. The method loadDataset() uses a QueryPager instance to get the current query string. The method loadNextDataset() asks for the query string that returns the next page of the paged query.

// Snippet from ArtistsListView.java

protected void loadDataset() {
    ApiCache.getArtistsInGenre(genreId, new PlaceResultsTask(), queryPager.getCurrentQueryString());
}

protected void loadNextDataset() {
    ApiCache.getArtistsInGenre(genreId, new PlaceResultsTask(), queryPager.getQueryStringForNextPage());
}

Conclusions

The example demonstrates how to implement a good UI using the components provided by LCDUI. In addition, a purpose of the example is to demonstrate how powerful the Tantalum 5 library is. The Tantalum 5 library makes it possible to concentrate on things that are valuable from the application's viewpoint.

Known issues

  • java.lang.NumberFormatException shown sometimes in the log during JSON parsing. This issue does not affect the application and does not cause any crashes.

  • The artist view might appear empty if the artist does not have any albums.

  • The user can navigate to new Artist views from the Similar artists view. These opened Artist views grow the view history and the device may run out of memory at some point.