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. 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.
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) { 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 website 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 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);
Along full touch, Java Runtime 2.0 for Series 40 also introduced many more nice new features. One of them is support for different orientations. The orientation changes do not work out of the box but the orientations need to be set manually. This is actually a good thing, since it gives developers better control. Check out the Enabling orientations section to see how it's actually done.
WeatherApp also utilises IconCommands
. To see
how they've been used, read the Adding IconCommand
section.
Java Runtime 2.0 for Series 40 additionally places the Back and exit buttons at the bottom right corner of a screen. The button is translucent and drawn on top of other screen content. In WeatherApp? this means that the button would overlap banner ads. That's why the ads are being scaled down a bit and moved to the left, so that they become fully visible. The issue is only present in portrait view, so in landscape the ads are centered normally.
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 Nokia Asha software platform devices, the setting can be changed in the Settings > Installed apps > Permissions.