CarOrBike – Getting a weather report

Note: in case you missed the overview, you can find it here.

The source code to this step is on GitHub, tagged as release 0.1.0: https://github.com/svenvermeulen/CarOrBike

In this part, I will obtain a weather report from a web service. I don’t care yet about the user’s actual location: at this point, coordinates are fixed. At the end of this part, the application will consist of an Activity with an “Update Weather” button, which causes the App to show the current temperature in a Toast.

Let’s get started.

Apixu

After a bit of quick research, I found apixu, a REST-based API which allows you to obtain a weather report in JSON format using a simple coordinate-based query. It offers a free plan which allows 10.000 API calls per month, more than enough for this application. Sign up, get an API key, and put it somewhere safe. I recommend keeping the key (and other secrets) outside your version control system. This is quite trivial, have a look here for a quick tutorial on configuring this.

The site has an interactive API explorer so you can toy around and see how to create requests, and what responses to expect.

RoboSpice

Initially, I made my calls to the apixu API using ASyncHttpClient for the REST call and JSONObject for parsing the returned JSON data. In the end, I decided to give RoboSpice a go. It allows me to

  • Make asynchronous web calls without activity-lifecycle related problems
  • Declare a POJO which is to be returned after the API call

The creators of RoboSpice have uploaded a nice infographic which explains how it works. If you like to dig a bit deeper, check out this explanation.

I include the library by adding the following lines to my app’s build.gradle:

compile 'com.octo.android.robospice:robospice-spring-android:1.4.13'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.13'

Creating the WeatherReport class

Apixu returns a weather report in JSON format. To be able to handle this easily in my app, I create a WeatherReport class which corresponds to the data returned by Apixu.

A sample JSON response from apixu:

{
    "location": {
        "name": "",
        "region": "",
        "country": "",
        "lat": 51.92,
        "lon": 4.48,
        "tz_id": "Europe/Amsterdam",
        "localtime_epoch": 1438712391,
        "localtime": "2015-08-04 18:19"
    },
    "current": {
        "last_updated_epoch": 1438711213,
        "last_updated": "2015-08-04 18:00",
        "temp_c": 20.0,
        "temp_f": 68.0,
        "condition": {
            "text": "Partly Cloudy",
            "icon": "http://www.apixu.com/static/weather/64x64/day/116.png",
            "code": 1003
        },
        "wind_mph": 17.4,
        "wind_kph": 28.1,
        "wind_degree": 250,
        "wind_dir": "WSW",
        "pressure_mb": 1015.0,
        "pressure_in": 30.4,
        "precip_mm": 0.0,
        "precip_in": 0.0,
        "humidity": 56,
        "cloud": 25,
        "feelslike_c": 20.0,
        "feelslike_f": 68.0
    }
}

As you can see, there are quite a few properties here. The ones I’m interested in are

  • name / region / country / coordinates of the Location
  • temperature of the “current” object, in degrees celsius

I thus create the following class, including only the properties I’m interested in:

package com.svenakos;

import org.codehaus.jackson.annotate.JsonIgnoreProperties;

public class WeatherReport {
    public WeatherReport.Location location;
    public WeatherReport.Forecast current;

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class Location
    {
        public String name;
        public String region;
        public String country;
        public double lon;
        public double lat;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class Forecast {
        public int last_update_epoch;
        public String last_updated;
        public double temp_c;
    }

}

The @JsonIgnoreProperties(ignoreUnknown = true) annotation allows me to declare only the fields I’m interested in; otherwise, I would have to declare all fields returned, or face a runtime Exception.

Setting up the Activity for RoboSpice

Time to “spice” the activity. As RoboSpice’s wiki will tell you, you need to override OnStart() and OnStop() and create a member variable to hold a reference to a SpiceManager:

protected SpiceManager spiceManager = new SpiceManager(JacksonSpringAndroidSpiceService.class);


@Override
protected void onStart() {
  super.onStart();
  spiceManager.start(this);
}

@Override
protected void onStop() {
  spiceManager.shouldStop();
  super.onStop();
}

Requesting data

For a quick test, I create a “get weather report” button with a simple OnClickHandler which calls the web service.

((Button)findViewById(R.id.get_current_weather_button)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // coordinates are fixed for now
        performForecastRequest(51.9225000, 4.4791700);
    }
});

Then, I perform the actual request using RoboSpice:

private void performForecastRequest(double lat, double lon) {
    MainActivity.this.setProgressBarIndeterminateVisibility(true);

    ForecastRequest request = new ForecastRequest(lat, lon);
    lastRequestCacheKey = request.createCacheKey();

    spiceManager.execute(request, lastRequestCacheKey, DurationInMillis.ONE_MINUTE, new ForecastRequestListener());
}

This bit introduces two new classes: ForecastRequest and ForecastRequestListener.

public class ForecastRequest extends SpringAndroidSpiceRequest<WeatherReport> {
    private double lat;
    private double lon;

    public ForecastRequest(double lat, double lon) {
        super(WeatherReport.class);
        this.lat = lat;
        this.lon = lon;
    }

    @Override
    public WeatherReport loadDataFromNetwork() throws Exception {
        String url = String.format(Locale.US, "http://api.apixu.com/v1/current.json?key=%s&q=%f, %f", BuildConfig.APIXU_API_KEY, lat, lon);
        return getRestTemplate().getForObject(url, WeatherReport.class);
    }

    public String createCacheKey() {
        return String.format(Locale.US, "apixu-weatherforecast.%f-%f", lat, lon);
    }
}

ForecastRequest extends the class SpringAndroidSpiceRequest, and represents a request to the apixu weather service. I pass the parameters I need to the constructor, which simply puts them in a couple of member variables. Then, I override the loadDataFromNetwork method, which is called by my SpiceManager when I tell it to execute the request.

The loadDataFromNetwork method formats the URL for the actual HTTP request, passing the values of the lat and lon variables into the URL query string. I specify Locale.US to make sure that ‘.’ is used for decimal separator, as opposed to ‘,’ which is the standard decimal separator in many European languages, but separates the parameters in URL query strings.

With the ForecastRequest class out of the way, let’s have a look at the ForecastRequestListener class:

private class ForecastRequestListener implements RequestListener<WeatherReport> {

    @Override
    public void onRequestFailure(SpiceException e) {
        Toast.makeText(MainActivity.this, String.format("Could not obtain data : %s", e.getMessage()), Toast.LENGTH_LONG).show();
    }

    @Override
    public void onRequestSuccess(WeatherReport forecast) {
        if (forecast==null) {
            Toast.makeText(MainActivity.this, "Got NULL forecast", Toast.LENGTH_LONG).show();
            return;
        }
        Toast.makeText(MainActivity.this, String.format("temperature (c): %f", forecast.current.temp_c), Toast.LENGTH_LONG).show();
    }
}

RoboSpice makes sure OnRequestFailure and OnRequestSuccess are called on the UI thread. The loadDataFromNetwork method is, of course, run in the background, so you can take your time to talk to a web service without blocking the UI. RoboSpice also makes sure that if you rotate your device (which destroys and then re-creates your Activity), the callback is performed correctly on your new instance.

Almost there. Don’t forget to add the ACCESS_NETWORK_STATE and INTERNET permissions so that RoboSpice can make its calls; also, add the following Service declaration to your manifest:

<service
    android:name="com.octo.android.robospice.JacksonSpringAndroidSpiceService"
    android:exported="false" />

The resulting app is not yet spectacular;A simple form with one lousy button which shows the current temperature in Rotterdam, the Netherlands. Way to go, Jaco. In the next part, I’ll be doing the visual design of the app.

Leave a comment