For information about the design and functionality of the MIDlet, see section Design.
For information about the key aspects of implementing the MIDlet, see:
The MIDlet main view is basically a Canvas
that draws forecasts for the selected day. All the
settings views and location-related views use plain platform-styled
LCDUI elements. View changing is handled by the ViewMaster
class, which also takes care of storing views into a view stack.
The view stack is used for the back navigation functionality.
The available positioning methods are:
Assisted GPS
Standalone GPS
Offline cell ID
Online cell ID
WLAN
The LocationUtil
extension of the Location API
can be used to specify the desired positioning method. However, LocationUtil
is only available on Series 40 devices with
Java Runtime 1.0.0 for Series 40 or newer. Symbian devices and earlier
Series 40 devices do not support LocationUtil
. Note
also that cell ID positioning can only be performed with the getLocation
method, whereas GPS positioning can be implemented
by using a LocationListener
. In other words, to be
able to listen for location changes when using the cell ID positioning
approach, the getLocation
method must be called frequently
in a loop. However, if the MIDlet is not signed, calling getLocation
in a loop causes an access prompt on every call. For more information
about listening for location changes, see article Best practises for listening to location updates
with Java ME in the Nokia Developer Wiki.
To ensure that
the MIDlet runs on devices that do not support LocationUtil
or the Location API, both APIs must be hidden from the code execution,
so that they are not loaded automatically. This means that neither
the com.nokia.mid.location
package nor the javax.microedition.location
package can be imported inside
classes that need to run on any device. Therefore, classes that use
either LocationUtil
or the Location API must be isolated
from the rest of the MIDlet. These classes can then be loaded at runtime
using the Class.forName(<PACKAGE>.<CLASS_NAME>)
method. This method throws an exception if a class is missing (for
example, LocationUtil
). However, the exception can
be caught, necessary actions can be taken, and the MIDlet can try
to use the next-best positioning method. For instructions on how to
implement this approach, see article How to use an optional API in Java ME in the
Nokia Developer Wiki.
The manual location search is used for finding cities when there is no automatic way to retrieve a location or when the user wants to know the weather forecast for a city other than the one they are currently located in. The manual location search works asynchronously and automatically sends an HTTP search request when the input in the text field changes:
/** * Performs a location search, if text input changes. * @param item */ public void itemStateChanged(Item item) { if (item instanceof TextField && !searchField.getString().equals( lastSearch)) { throttleSearch(); } }
To limit the number of searches, the MIDlet implements a mechanism that delays the search by 1000 milliseconds. The timer is reset every time the user types in the text field. This is called throttling. The search additionally gives the user a "search as you type" kind of functionality. The search is implemented as follows:
/** * Delays location search and cancels a possible pending search */ public void throttleSearch() { if (throttle != null) { throttle.cancel(); } throttle = new Timer(); throttle.schedule(new TimerTask() { public void run() { searchLocations(); cancel(); } }, 1000); }
The search view consists of a Form
, a TextField
, and a set of CustomItems
. CustomItems
are used because a regular List
cannot be appended to a Form
other
than a ChoiceGroup
, which, in turn, can only contain
check boxes and radio buttons.
All locations used by the MIDlet are stored in
the recent locations view. Locations are saved in a RecordStore
, so that they persist to the next time the user runs the MIDlet.
The following code snippet shows how to save and load locations.
Note: On Symbian devices, the elements are stored in opposite order than on Series 40 devices.
private static void saveLocations() { try { RecordStore.deleteRecordStore("locations"); // Clear data } catch (Exception e) { /* Nothing to delete */ } try { RecordStore rs = RecordStore.openRecordStore("locations", true); int count = recentLocations.size(); for (int i = 0; i < count; i++) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); Location lctn = (Location) recentLocations.elementAt(i); dos.writeUTF(lctn.city); dos.writeUTF(lctn.country); byte[] b = baos.toByteArray(); // Add it to the record store rs.addRecord(b, 0, b.length); } rs.closeRecordStore(); } catch (RecordStoreException rse) { rse.printStackTrace(); } catch (IOException ioe) { ioe.printStackTrace(); } } // ... private static void loadLocations() { try { boolean symbian = System.getProperty("microedition.platform").indexOf("S60") > -1; RecordStore rs = RecordStore.openRecordStore("locations", true); RecordEnumeration re = rs.enumerateRecords(null, null, true); while (re.hasNextElement()) { int id = re.nextRecordId(); ByteArrayInputStream bais = new ByteArrayInputStream(rs.getRecord(id)); DataInputStream dis = new DataInputStream(bais); try { Location location = new Location(); location.city = dis.readUTF(); location.country = dis.readUTF(); if (symbian) { // On Symbian the elements have been stored in opposite order compared to S40 recentLocations.addElement(location); } else { recentLocations.insertElementAt(location, 0); } } catch (EOFException eofe) { eofe.printStackTrace(); } } rs.closeRecordStore(); } catch (RecordStoreException rse) { rse.printStackTrace(); } catch (IOException ioe) { ioe.printStackTrace(); } }
You can request weather forecast data using World Weather Online's free Local Weather API with a simple GET request. The GET request must include the location for which you want to request the forecast data, the response format, the number of days for which you want to request the forecast data, and your API key. The location is defined as a city name and optionally country name, IP address, ZIP code, or geographic coordinates. The WeatherApp MIDlet uses both city and country names and latitude-longitude pairs for requesting forecast data, depending on whether the positioning is performed automatically or based on manual location search. The WeatherApp MIDlet uses the JSON format, but the forecast data is also available in XML and CSV formats. You can obtain the API key from World Weather Online. The web site also provides a free Local Weather API Request Builder for generating request URLs.
The following is an example of a weather forecast data request:
http://free.worldweatheronline.com/feed/weather.ashx?q=London,United+Kingdom&format=json&num_of_days=5&key=<YOUR API KEY>
World Weather Online also provides the City/Location Search API. The WeatherApp MIDlet uses the API for searching actual locations based on a city name or part of a city name. The free version of the API limits the maximum number of possible responses to three.
The following is an example of a city location request:
http://www.worldweatheronline.com/feed/search.ashx?query=London&format=JSON&num_of_results=3&key=<YOUR API KEY>
JSON parsing is simple to implement with the org.json.me
library. However, you need to keep a few things
in mind. Firstly, JSON is parsed using JSONArrays
and JSONObjects
, so keep an eye on the square brackets
to distinguish when a JSONArray
needs to be used
over a JSONObject
. The following JSON is part of
a weather forecast response from World Weather Online.
{ "data": { "current_condition": [ { "cloudcover": "75", "humidity": "87", "observation_time": "02:41 PM", "precipMM": "3.0", "pressure": "991", "temp_C": "8", "temp_F": "46", "visibility": "10", "weatherCode": "302", "weatherDesc": [ { "value": "Moderate rain" } ], "weatherIconUrl": [ { "value": "http://www.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0018_cloudy_with_heavy_rain.png" } ], "winddir16Point": "ESE", "winddirDegree": "120", "windspeedKmph": "24", "windspeedMiles": "15" } ], "request": [ { "query": "London, United Kingdom", "type": "City" } ], "weather": [ { "date": "2012-04-23", "precipMM": "6.3", "tempMaxC": "9", "tempMaxF": "48", "tempMinC": "4", "tempMinF": "38", "weatherCode": "266", "weatherDesc": [ { "value": "Light drizzle" } ], "weatherIconUrl": [ { "value": "http://www.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0017_cloudy_with_light_rain.png" } ], "winddir16Point": "ESE", "winddirDegree": "109", "winddirection": "ESE", "windspeedKmph": "22", "windspeedMiles": "14" }, . . . ] } }
Secondly, a response can contain required and optional
keys. Required keys can be parsed using the getString
method, which throws
a JSONException
if a required key cannot be found.
The optString
method comes handy
when a specific key is optional in the response. The method does not
interrupt the parsing by throwing a JSONException
but returns a default value instead. The default value can be set
through a method parameter. If the default value is not specified,
the method returns an empty string. The methods in the following code
snippet parse a forecast response and turn it into a collection of Weather
objects. For more information, see the org.json
reference.
/** * Parses JSON response * @param response JSON response as string * @throws ParseError */ public void parse(String response) throws ParseError { try { JSONObject obj = new JSONObject(response); if (obj.isNull("data")) { return; } JSONObject data = obj.getJSONObject("data"); JSONArray currentCondition = data.getJSONArray("current_condition"); forecasts.addElement(parseWeather(currentCondition.getJSONObject(0))); JSONArray upcomingConditions = data.getJSONArray("weather"); int length = data.length(); for (int i = 0; i < length; ++i) { forecasts.addElement(parseWeather(upcomingConditions.getJSONObject(i))); } } catch (Exception e) { throw new ParseError(e.getMessage()); } } /** * Parses a single day from response * @param weather JSONObject containing weather data for one day * @return Populated Weather object * @throws JSONException */ private Weather parseWeather(JSONObject weather) throws JSONException { Weather w = new Weather(); w.humidity = weather.optString("humidity", ""); w.temperatureC = weather.optString("temp_C", ""); w.temperatureF = weather.optString("temp_F", ""); w.minTempC = weather.optString("tempMinC", ""); w.minTempF = weather.optString("tempMinF", ""); w.maxTempC = weather.optString("tempMaxC", ""); w.maxTempF = weather.optString("tempMaxF", ""); w.windDirectionDegrees = weather.getString("winddirDegree"); w.windDirectionPoints = weather.getString("winddir16Point"); w.windSpeedKmph = weather.getString("windspeedKmph"); w.windSpeedMph = weather.getString("windspeedMiles"); JSONArray description = weather.getJSONArray("weatherDesc"); w.description = description.getJSONObject(0).getString("value"); JSONArray iconUrl = weather.getJSONArray("weatherIconUrl"); w.iconUrl = iconUrl.getJSONObject(0).getString("value"); return w; }
The MIDlet employs in-app advertising in the form of banners and full-screen ads. In-app advertising allows you to monetize your application by showing ads provided by Inneractive. Ads are monetized through impressions and clicks. The click-through rate, which is the ratio between impressions and clicks, also affects the revenue.
The user must be able to click a banner. When the banner is clicked, an advertisement endpoint URL is launched inside a browser. On touch devices, it is easy to just tap the banner, but non-touch devices additionally require focus handling, which allows the user to first select the banner and then click it.
In the WeatherApp MIDlet, a full-screen ad is shown when the user is about to exit the MIDlet. In addition, banner ads are shown at the bottom of the screen. One banner is shown for 60 seconds and then changed to a new one with a sliding animation. Since the average usage time of the MIDlet is likely fairly short, the ads need to be updated quite frequently. Inneractive recommends using an interval of about 2-3 minutes.
It is recommended that you retrieve ads inside a worker thread, so that the main thread does not get blocked. The following code snippet shows how this is implemented in the WeatherApp MIDlet.
private boolean running = false; private AdListener listener = null; private static MIDlet context; /** * Thread executing the ad retrieval */ private class Worker extends Thread { private int task = 0; private long interval = -1; public synchronized void run() { running = true; while (running) { if (Network.isAllowed()) { switch (task) { case DISPLAY_FULL_AD: if (listener == null) { IADView.displayInterstitialAd(context); } else { IADView.displayInterstitialAd(context, listener); } break; case GET_BANNER_AD: Ad ad = null; try { ad = new Ad(IADView.getBannerAdData(context)); } catch (IllegalArgumentException iae) { } if (listener != null) { listener.bannerReceived(ad); } break; } } try { if (interval < 0) { task = IDLE; wait(); } else { wait(interval); } } catch (InterruptedException ex) { } } } public synchronized void doTask(int task, long interval) { if (context == null) { return; } this.task = task; this.interval = interval; notify(); } }
Adding ads to the MIDlet requires some modifications to the layout, since there must be some extra space for the banners. To accommodate the banners, an additional layout was designed for landscape mode and smaller screen sizes. In addition, it must be noted that full-screen ads take up a lot of memory, so it is advisable to free as much memory as possible before showing an ad.
To implement in-app advertising, you need the Ad SDK, which can be downloaded from the Inneractive web site. The Ad SDK contains examples that demonstrate how to use it. The Ad SDK also contains an ad placement strategy guide and a step-by-step guide for adding in-app advertising to your application.
The Ad SDK requires that you specify some application attributes in the MIDlet JAD file, for example:
IA-X-appID
(mandatory)
The IA-X-appID
attribute specifies the application ID. The ID
is provided during the Inneractive registration process. It is used
for directing ad revenue from impressions and clicks to you. IA-X-appID
is the only mandatory attribute required by the
Ad SDK. The WeatherApp MIDlet uses IA_GameTest
as
its application ID.
IA-X-distID
(optional)
The IA-X-distID
attribute specifies the distribution ID and
is used for validating ad requests, optimizing performance, and determining
ad type. The value 551
must be used for banner ads
and full screen ads.
IA-X-keywords
(optional)
The IA-X-keywords
attribute is used for describing
topics for ads, so that only ads relevant to the user's specific session
are shown.
IA-X-gpsCoordinates
and IA-X-location
(optional)
The IA-X-gpsCoordinates
and IA-X-location
attributes are used for ad localization.
For more information about these and other optional attributes, see the Ad SDK documentation.
The MIDlet has different visual styles for night and day modes. The mode is selected according to the weather icon URL in the HTTP response, meaning that ultimately World Weather Online decides when day time ends and night begins.
The Series 40 full touch version of the
MIDlet uses the Orientation API to detect display orientation changes and
adjust its UI orientation accordingly. To adjust the UI orientation
using the Orientation API, the Nokia-MIDlet-App-Orientation
attribute must be declared with the value manual
in the MIDlet JAD file:
Nokia-MIDlet-App-Orientation: manual
To ensure that the MIDlet runs on devices that do not support
the Orientation API, the API, like the LocationUtil
class and Location API, is wrapped
so that it is isolated from the rest of the MIDlet and not loaded
automatically when the MIDlet is run. The MIDlet uses the custom OrientationImpl
class to implement the actual orientation
support, and the custom Orientation
class to load
the implementation.
The OrientationImpl
class
uses the static Orientation.addOrientationListener
method
to register an OrientationListener
, and the static Orientation.setAppOrientation
method to set
the UI orientation when the display orientation changes. The Orientation.setAppOrientation
method is called from the
mandatory displayOrientationChanged
callback method,
which every OrientationListener
must implement, and
which is called every time the display orientation changes. The following
code snippets shows the implementation of the OrientationImpl
class.
class OrientationImpl extends Orientation implements com.nokia.mid.ui.orientation.OrientationListener { OrientationImpl() { // Listen for orientation events com.nokia.mid.ui.orientation.Orientation.addOrientationListener(this); } /** * Deliver sizeChanged event to listener * @param newDisplayOrientation */ public void displayOrientationChanged(int newDisplayOrientation) { com.nokia.mid.ui.orientation.Orientation.setAppOrientation(newDisplayOrientation); } }
For instructions on how to implement the wrapper approach, see article How to use an optional API in Java ME in the Nokia Developer Wiki.
The Series 40 full touch version of the MIDlet
uses the IconCommand
class to map a custom "Add" command in
the form of a plus icon to action button 1 in the header bar of the full touch UI. The IconCommand
allows the
user to add new locations to the recent locations view.
Figure: IconCommand as a plus icon in the header bar (action button 1)
To ensure that the MIDlet runs on devices that do not support IconCommands
, the IconCommand
implementation
is wrapped inside a factory class that can be used to produce IconCommands
when they are supported by the device. If the
factory class returns an IconCommand
, it is used
to replace the default Command
mapped to action button
1.
The MIDlet uses the abstract IconCommandFactory
class to create IconCommands
. The static IconCommandFactory.getIconCommand
method returns the IconCommand
instance. The method is static so that the IconCommandFactory
class does not need to be explicitly
instantiated by the caller. However, the IconCommandFactory
class is instantiated in a static context, which allows the class
to use the IconCommandFactoryImplementation.createIconCommand
method internally to create the actual IconCommand
instance returned by the IconCommandFactory.getIconCommand
method. The IconCommandFactory
class implements
only one of the four possible constructors for IconCommand
, but the other three could be easily implemented in the same manner.
The following code snippet shows the implementation of the IconCommandFactory
class.
import javax.microedition.lcdui.Command; import javax.microedition.lcdui.Image; /** * Procudes icon commands, if the platform supports them. * IconCommand is supported from Java Runtime 2.0.0 for Series 40 onwards. */ public abstract class IconCommandFactory { private static IconCommandFactory implementation = null; /** * Try to instantiate IconCommandFactory */ static { try { Class.forName("com.nokia.mid.ui.IconCommand"); Class c = Class.forName("com.nokia.example.weatherapp.components.IconCommandFactoryImplementation"); implementation = (IconCommandFactory) (c.newInstance()); } catch (Exception e) { // Icon commands not supported } } protected IconCommandFactory() { } /* * Creates an IconCommand */ public abstract Command createIconCommand(String label, Image upState, Image downState, int type, int priority); /** * Returns new IconCommand or null, if IconCommands are not supported */ public static Command getIconCommand(String label, Image upState, Image downState, int type, int priority) { if (implementation == null) { return null; } return implementation.createIconCommand(label, upState, downState, type, priority); } public static boolean iconCommandsSupported() { return implementation != null ? true : false; } } /** * Creates an IconCommand. Hides the usage of IconCommand from the linker */ class IconCommandFactoryImplementation extends IconCommandFactory { protected IconCommandFactoryImplementation() { } public Command createIconCommand(String label, Image upState, Image downState, int type, int priority) { return new com.nokia.mid.ui.IconCommand(label, upState, downState, type, priority); } }
The following code snippet shows how to create an IconCommand
by calling the factory class, and use the IconCommand
to replace the default Command
.
Resources r = Resources.getInstance(locationsView.getWidth(), locationsView.getHeight()); if (IconCommandFactory.iconCommandsSupported()) { addCmd = IconCommandFactory.getIconCommand("Add", r.getAddIcon(), r.getAddIcon(), IconCommand.SCREEN, 1); } locationsView.addCommand(addCmd);
While parsing JSON values is straightforward with
the org.json.me
library, retrieving the location
is a bit more complicated. Extra prompts and long waiting times can
irritate users, which is why it is important to evaluate the best
method for each use case. Cell ID positioning is fast, but inaccurate,
whereas GPS can be much more accurate, but can introduce longer delays.
In addition, the application flow must be designed so that the application
can flexibly use a different positioning method if the preferred one
is unavailable on a particular device. Finally, the number of prompts
must be kept to a minimum. Asking the same thing twice is already
one time too many. This applies both to network access prompts and
positioning prompts.
CustomItems
provide a
way to handle UI elements that are not possible to implement with
LCDUI elements. However, there is a lot of variation between different
platforms and platform versions in terms of how they implement CustomItems
. For example, retaining the native platform
look on a CustomItem
can be difficult to accomplish.
When mist, fog, or black clouds are forecast, the visual style always follows the day mode.
On some devices, network access is set to "ask always" by default. On Series 40 devices, the setting can be changed by scrolling down to the MIDlet in question and selecting Options > Application access. On Symbian devices, the setting can be change in the Application Manager.
In the search view, existing list items do not change size when changing orientation. This is because of a bug in the Symbian platform, which would turn the list items into white boxes instead.
On Symbian Belle devices, softkey labeling is not possible.
In a list view, the right softkey can be either an "OK" tick mark
or an options menu icon. For example, to prevent the Add
command from turning into a tick mark in the locations view, the
command type has been set to EXIT with a lower priority than the existing
BACK button. This way the command appears under the options menu.