For information about the design and functionality of the MIDlet, see section Design.
The diagram below shows an overview of the application architecture and the general relationships between the components of the application.
Figure: Overview of the application architecture
The Main class is both the entry point for the application and central controller for the data-intensive views (LinksView and CommentsView). The boilerplate for switching between views is intentionally minimal: the base class BaseFormView contains the methods setDisplay() and show() for changing the active view and telling the view it has been shown, and the rest of the display logic happens in the views themselves.
These two views are created in many instances: a new LinksView is created whenever a new category is selected, and a new CommentsView when a Link is selected. As the instances of these two view classes are cached by the MIDlet class in its ViewCache, navigating the application between different views should be fluent as long as there is memory available. When the application runs out of memory, the cached views will automatically become garbage collected by the environment, and have to be populated again.
Besides these two views, there are other lesser views that are created within individual views as needed. For example, the logic contained in CategorySelectView and LoginView is closely related to their parent views, and handled by them. Communication between views and view items is done with Listeners. For example, when a comment is selected in CommentDetailsView, a new instance of CommentDetailsView is created, passing its constructor the selected comment item, and two listeners: a CommentDetailsBackListener to determine what will happen when the view is closed, and a VoteListener to specify what to do when the comment item has been voted on (in the other view).
CommentDetailsView cv = new CommentDetailsView( item, new CommentDetailsBackListener() { public void backCommanded(boolean commentAdded) { // New comment added, refreshing if (commentAdded) { link.setNumComments(link.getNumComments() + 1); refresh(); } setDisplay(self); } }, new VoteItem.VoteListener() { public void voteSubmitted(int vote) { item.setVote(vote); } } );
All network operations are based on HTTP requests. Most requests use the GET method, while those dealing with authentication and submitting user-input data use POST. All the network operations are instances of the abstract HttpOperation class. It is an easily extendable base class for everyday HTTP operations and supports the following features:
running HTTP operations in parallel
enqueuing and aborting operations
GET and POST request methods
data in request body
text and binary context types
nave cookie support (always stores and sends all cookies, unless Operation specifically disables them)
The implementations of an HttpOperation typically need to implement 3 things:
the method getUrl() that will tell the HttpClient which URL to request
the method responseReceived(byte[] data) that will be called when the HttpClient gets a response from the server
a custom listener (passed in the constructor) to signal the caller of the request results
Any HttpOperation is launched by simply calling start(). The default implementation of start() enqueues the operation in the application-wide queue of HttpClient, where it will be executed when its turn comes. This is how an operation for loading an image is triggered in the class ImageLoader:
new ImageLoadOperation(url, new ImageLoadOperation.Listener() { public void imageReceived(String url, byte[] data) { Image image = defaultImage; try { image = Image.createImage(data, 0, data.length); } catch (IllegalArgumentException e) {} catch (NullPointerException e) {} if (cache != null) { cache.put(url, image); } listener.imageLoaded(image); } }).start();
The most simple example of an HttpOperation could be that of ImageLoadOperation, which only needs to fetch and pass forward any data received from a static URL. In its bare essentials it looks like the following:
public final class ImageLoadOperation extends HttpOperation { public interface Listener { public void imageReceived(String url, byte[] data); } private final String url; private final Listener listener; public ImageLoadOperation(String url, Listener listener) { this.url = url; this.listener = listener; } public final String getUrl() { return url; } public final void responseReceived(byte[] data) { finished = true; listener.imageReceived(url, data); } }
Secure HTTPS connections are used similarly as described above, with the connection object being cast to an instance of HttpsConnection. The Reddit SSL certificate didn't work properly with the devices tested with, so a standard HTTP connection was used. However, it is always recommended to use a secure connection when available.
The SessionManager class takes care of loading and saving user session data in the persistent storage (RMS).
The following data is persistent:
the currently selected category
username (after having logged in)
modhash assigned to the user upon login by Reddit
all the cookies Reddit has stored
Reddit sessions rely upon two items: the modhash and a reddit_session cookie. The naive cookie support implemented in the scope of this application doesn't care what cookies it gets from Reddit - it always saves and sends all of them. Basically, after a successful login, having received both a modhash and a reddit_session cookie, the login should be valid "indefinitely". As a consequence, when both the modhash and the cookies are stored, the user should stay logged in until he or she decides to log out, upon which the modhash and cookies are cleared from the storage.
The Reddit API is subject to change on short intervals and some things might change without prior warning. Quoting the Reddit API page on Github:
Changes to this API can happen without warning. Use the Firefox add-on LiveHeaders or the Webkit inspector's Network section to see what fields are being posted this week. The JSON replies can also change without warning.
JSON was used for all data requests where available. During the implementation of this application, it was noted that the responses sent by the Reddit API differed considerably from each other in terms of the usefulness of the response. Consider these examples:
A successful login action:
{"json": {"errors": [], "data": {"modhash": "u4abc21302316ad40013feb16cfccb0b11b786596e5194de14", "cookie": "1234567,2011-07-12T14:53:59,0200b365fa02c61f9532ab244b214bd481941492"}}}
A successful vote action:
{}
A successful comment action:
{ "jquery":[ [0, 1, "call", ["#form-t3_iqhw5ili"]], [1, 2, "attr", "find"], [2, 3, "call", [".status"]], ... [20, 21, "attr", "hide"], [21, 22, "call",[]] ] }
While all the responses are in JSON format, only the login response contains actual data (most importantly, the modhash). A successful vote action is an empty JSON object, and a successful comment action only contains special instructions for showing and hiding DOM nodes on Reddit.com, and are next to useless on a 3rd-party client.
The UI relies on standard LCDUI components. All views are based on the standard Form class, which is automatically made vertically scrollable if the views' content does not fit the height of the screen. This is a good fit for the text-intensive nature of the application. By using default components, we also get drawing, scrolling, and related touch gestures "for free".
While letting the platform handle the drawing and scrolling of the views makes it easy to add new content, it also sets some constraints related to how much the application style can be customised. For example, all Items inserted in a Form-based view will have a border line when they are focused. The items will also have a transparent background, which means that the background image displayed will be that of the theme selected in the phone, and that the only text colour that works reliably is the foreground colour specified in the theme itself. Each item will also always have some pixels of invisible non-removable margin around it, which practically makes it impossible to fully customise the items.
An alternative to using standard Forms would be to implement a full custom UI with Canvas or GameCanvas. That would mean, however, implementing quite a lot of boilerplate code for drawing, scrolling, and gestures. This approach was not chosen for the scope of this application. There is, however, a sweet spot between a traditional LCDUI and a full custom UI: populating the Form-based views with custom view items, that is, instances of the CustomItem class. This compromise is easy to implement by leveraging the existing Form functionality, while still being able to create versatile UI components and style them a bit.
The CustomItem components are like small canvases stacked on top of each other: by implementing the paint() method, the component can be laid out in whatever way you like. Combined with the possibility to use custom font sizes in recent Nokia Series 40 phones, it allows for a nice degree of customisation while still benefiting from the components provided by the platform.
Normally, we are limited to the foreground color of the Series 40 theme. However, it is possible to have a secondary text colour available for making the application text more pleasant for the eyes and a easier to read. With a formula that returns a "perceived brightness" value for a given colour, we calculate the brightness of the current foreground colour and create a secondary colour by adjusting the primary colour a little: either darken a light colour, or lighten a dark colour. For example, with a white foreground colour, we would get a light gray secondary colour. Respectively, with a black foreground colour, the secondary colour would be dark gray.
An example of the same view on a light and a dark theme, and the secondary colours in practice on a Series 40 touch and type device (shown in link score, vote button texts, number of comments):
Figure: Light theme with darkened secondary colour (left) and dark theme with lightened secondary colour (right)
Comments, written by Reddit users in response to links and other comments, are the most frequently created items in the application. As there might be replies to existing comments, the list of comments is essentially a tree structure where each comment has zero or more child comments. While the comment tree might be indefinitely deep in theory, most of the time comment trees will be less than 10 levels in depth.
When loading comments from Reddit, the following steps are taken:
Request a list of comments from Reddit.
Parse the JSON response into CommentThing objects. This is done with a recursive method call.
For each CommentThing object created, create a CommentItem view item for representing it in the UI.
For representing the comment tree, we only need to know the level (its depth in the tree) of a single comment. As a result, the application flattens the received JSON object tree into a simple Vector of CommentThing objects. The CommentItem object that represents the comment contents in the UI holds an instance of the original CommentThing, and is able to draw the comment based on its depth, so that the outcome still looks like a tree.
Incremental loading of comments
Because parsing JSON and creating objects is data intensive, it will take a while to complete on Series 40 phones. This is why parsing comments is handled in separate background threads and the view is signaled asynchronously of the parsing results, using Listeners. Also, there might be thousands of comments for a link, and loading all of them at once would not only take time to be parsed, but also the size of the JSON response itself might be several hundred kilobytes.
The approach taken in this application is to initially load some tens of comments, with the option to load more if there are undisplayed comments. In addition to containing comment data, the Reddit API response also contains "more" elements to indicate that there are more comments.
This is what a comment item with 'more' comments available might look like:
{ "kind": "t1", "data": { "body": "Dogs always know. My husky is the same way towards my 17 month old daughter. ", "subreddit_id": "t5_2qh1o", "author_flair_css_class": null, "created": 1334833175, "replies": { "kind": "Listing", "data": { "modhash": "", "children": [ { "kind": "more", "data": { "id": "c4e2bet", "children": [ "c4e2bet" ], "name": "t1_c4e2bet" } } ], "after": null, "before": null } }, (... rest of the content omitted ...) } }
Whenever a "more" item is encountered, a special MoreCommentsItem is used to indicate that more comments can be loaded at that specific index. By selecting the item, the application will ask Reddit for the hidden comments, and update the UI with new data. This works very conveniently, as the original scrolling position is retained, and new content will appear in the view as soon as it has been processed.
The comment might have more than one children hidden. If that is the case, only the comments contained under the first child ID will be loaded, and a new 'more' item will be created. This is because the number of comments the API will return for a single child ID is not known in advance: it might be 2, or it might as well be 400.
Figure: Before loading more comments (left) and after loading more comments (right)
For information about porting the MIDlet to Series 40, see section Porting.