Skip to content
This repository

This example shows you how to create a simple location-aware weather forecast MIDlet that also supports in-app advertising. The MIDlet shows a four-day forecast with temperatures, relative humidity, wind speed, and wind direction. The MIDlet retrieves the weather forecast information based on the device's current location, which the MIDlet determines using either cell ID or GPS positioning.

image image image

WeatherApp introduces in-app advertising in form of banners and full-screen ads. In-app advertising generates better revenue for your application. All the ads are provided by Inneractive. World Weather Online APIs are used for both weather data and location search data. The application uses the org.json.me library for parsing JSON.

Compatibility

  • Compatible with Nokia Asha software platform 1.0 and Series 40 phones. Note: The phone needs to support the maximum Java heap size and a JAR file size of at least 1 MB. Network connectivity is also required.
  • Tested on Nokia Asha 501, Nokia Asha 311, Nokia Asha 306, Nokia Asha 303, Nokia Asha 302, Nokia Asha 200 and Nokia X3-02.
  • Developed with Nokia Asha SDK 1.0.

Using an online weather service with JSON

The user also explicitly specify the location for which to retrieve the weather forecast information. The MIDlet uses World Weather Online APIs for retrieving both weather data and location search data, and the org.json.me library for parsing the JavaScript Object Notation (JSON) values returned by the World Weather Online APIs. In addition, the MIDlet employs in-app advertising in the form of banners and full-screen ads.

In short, this example demonstrates how to:

  • Use the Location API for cell ID and GPS positioning
  • Use World Weather Online APIs for retrieving weather and location search data
  • Parse JSON values
  • Implement in-app advertising

Prerequisites

You need the following to develop and test this MIDlet:

  • Nokia Asha SDK 1.0.
  • Nokia Asha software platform 1.0 or Series 40 phone with CLDC 1.1, MIDP 2.1 APIs.
  • World Weather Online API key.
  • Nokia Ad Exchange API key.

Please note that you need to acquire your own API keys for using the World Weather Online API (http://www.worldweatheronline.com) and Nokia Ad Exchange API (https://nax.nokia.com/‎). Replace the placeholder keys in com.nokia.example.weatherapp.network.Keys class with the acquired keys.

For more information about the MIDlet, see the following subsections on this page:

  • Design for details about the design and functionality of the MIDlet.
  • Implementation for instructions on how to implement the classes that make the MIDlet.

You can download the project files for the MIDlet from featured downloads.

Design

WeatherApp has been designed to scale across a range of Nokia Asha and Series 40 devices. The UI graphics are available in two sizes and there are three different layouts for different screen sizes. Layouts have been tested to scale on the following screen resolutions: 128x160, 240x320, 320x240, 240x400, 400x240, 360x640, 640x360, and 640x480 pixels.

The MIDlet uses the fastest available positioning method, starting with cell ID positioning. If cell ID positioning cannot be used, the MIDlet tries to retrieve the location using GPS. If GPS is not available either, the user can enter the location manually through the location search. The location search supports auto-complete. The MIDlet has been designed so that missing location APIs do not cause any errors: even if the device does not support location APIs, the MIDlet still runs gracefully.

The MIDlet uses World Weather Online APIs for retrieving both weather forecast data and location search data. Due to licensing terms, the World Weather Online API key is not published in the source code of the MIDlet. If you compile the MIDlet from the source code, either include your own API key in the Keys.java class file or run the MIDlet in test mode. In test mode, the MIDlet uses static example forecast data instead of real forecast data. The application is automatically run in test mode if the default placeholder API keys are used.

The Nokia Asha and Series 40 full touch version of the MIDlet also uses the Orientation API to detect display orientation changes and adjust its UI orientation accordingly.

imagePlaceholder  imagePlaceholder

Implementation

Basic UI structure

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.

Determining the device location

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 available only 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 the 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(.) 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 the article How to use an optional API in Java ME in the Nokia Developer Wiki.

Manual location search

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.

Recent locations

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 the 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();
        }
    }

Requesting weather forecast data and city locations

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

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;
    }

In-app advertising

The MIDlet employs in-app advertising in the form of banners and full-screen ads. In-app advertising allows you to monetise your application by showing ads provided by Nokia Ad Exchange. Ads are monetised 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. The recommended interval is 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, Keys.NAX);
                            }
                            else {
                                IADView.displayInterstitialAd(context, Keys.NAX, listener);
                            }
                            break;
                        case GET_BANNER_AD:
                            Ad ad = null;
                            try {
                                ad = new Ad(IADView.getBannerAdData(context, Keys.NAX));
                            }
                            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 Nokia Ad Exchange (NAX) plugin which is included in Nokia Asha SDK 1.0. The NAX library contains examples that demonstrate how to use it.

Night and day modes

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.

Setting the UI orientation

The Nokia Asha and 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);
        System.out.println("orientationInit");
    }

    /**
     * Deliver sizeChanged event to listener
     * @param newDisplayOrientation
     */
    public void displayOrientationChanged(int newDisplayOrientation) {
        System.out.println("orientationChanged");
        com.nokia.mid.ui.orientation.Orientation.setAppOrientation(newDisplayOrientation);
    }
}

For instructions on how to implement the wrapper approach, see the article How to use an optional API in Java ME in the Nokia Developer Wiki.

Adding an IconCommand

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.

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.

imagePlaceholder

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);

Concluding notes

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.

Known issues

  • 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.

Something went wrong with that request. Please try again.