I’m teaching an Android development class this week, and one of our primary references is the book Android 6 for Programmers, 3rd edition, which was released last December. One of the examples in the book accesses the Open Weather Map RESTful web service and builds a UI around the results, which is pretty much the default Android developer app.
The app accesses Open Weather Map by creating an instance of the URL
class, invoking openConnection
on the result, and downloading the response using the resulting InputStream
. It then parses the response using various classes in the org.json
package, including JsonObject
and JsonArray
.
As you might imagine, this is a tedious way to solve the problem. Java is already verbose; adding Android makes it worse, and then doing networking and JSON parsing “by hand” is just too much. As a teaching example it’s fine, but I wouldn’t recommend that as a long-term solution.
For RESTful web services, I’ve been a fan of the Spring for Android project, which includes a class called RestTemplate
that has a method called getForObject
. Once you map a set of Java classes to the expected JSON response, accessing the web service becomes a simple one-liner. Much better.
The problem, however, is that the Spring for Android project is now dormant to the point of being inactive. The 1.0.1 release is dated December, 2012, and the 2.0.0 M3 milestone hasn’t changed in years. That makes me reluctant to keep recommending it to new Android developers.
Instead, the primary library for working with RESTful services in Android appears to be Retrofit, from Square. It’s very powerful and current, and the only problem is that the documentation is, shall we say, thin.
I wanted to show the students in my class how to rewrite the book app to use Retrofit instead of doing the low-level networking and JSON parsing. That meant I had to experiment with the library, which is something I’d been planning to do for years but never actually did. The good news is that Retrofit can be used in a stand-alone Java app, so I could try it out myself before worrying about the Android aspects of the problem.
As often happens, that lead me to Groovy. Most Groovy apps are combinations of both Groovy and Java, and I like to say that while Java is good for tools, libraries, and basic infrastructure, Groovy is good for everything else. While it’s unlikely I can convince my students to use Groovy in their apps (it’s a very conservative company), I could certainly use it myself during my learning process.
The book code eventually produced a Java class called Weather
, used to hold formatted strings for the day of the week, the min and max temperatures forecasted for that day, the humidity percent, a String
description of the weather, and a URL to an icon showing the weather (sunny, cloudy, or whatever). My goal was to use Retrofit to access the Open Weather Map API, download the resulting JSON response, convert it to classes, and then create an instance of Weather
for each of the forecast days.
First I created a new Gradle-based project that allowed me to mix Java and Groovy together. Here’s the build file, showing the Retrofit dependencies.
[code language=”groovy”]
apply plugin: ‘groovy’
sourceCompatibility = 1.8
repositories {
jcenter()
}
dependencies {
compile ‘org.codehaus.groovy:groovy-all:2.4.6’
compile ‘com.squareup.retrofit2:retrofit:2.0.1’
compile ‘com.squareup.retrofit2:converter-gson:2.0.1’
testCompile ‘junit:junit:4.12’
}
[/code]
I’m using the Gson converter, which automatically converts the JSON response to a set of classes once I’ve defined them.
Step 1 in any mapping operation is to look at the form of the JSON response. Here’s an abbreviated sample, from http://api.openweathermap.org/data/2.5/forecast/daily?q=Marlborough,CT&units=imperial&cnt=16&APPID=d82ee6zzzzzzz .
[code language=”javascript”]
{"city":{"id":4844078,"name":"Terramuggus","coord":{"lon":-72.47036,"lat":41.635101},"country":"US","population":0},"cod":"200","message":0.0156,"cnt":16,"list":[{"dt":1460044800,"temp":{"day":51.48,"min":49.69,"max":51.48,"night":49.69,"eve":51.48,"morn":51.48},"pressure":984.62,"humidity":97,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":9.98,"deg":170,"clouds":88,"rain":3.8}, { … }, … ]}
[/code]
After the basic info, there is an array of 16 JSON objects representing the data I need, one for each day. (Note: to do this yourself, you’ll need to replace the APPID
with your own, which you can get at the Open Weather Map site.)
Working top down, here is the set of POGOs (Plain Old Groovy Objects) I created to map to just the few parts I needed:
[code language=”groovy”]
class Model {
WeatherData[] list
}
class WeatherData {
long dt
TempData temp
int humidity
WeatherInfo[] weather
}
class TempData {
double min
double max
}
class WeatherInfo {
String description
String icon
}
[/code]
To use Retrofit, I did what I normally do, which is to write a Groovy script and then eventually turn it into a class. That makes it easy to integrate with existing Java classes. Here’s the class I eventually created:
[source language=”groovy”]
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class DownloadForecast {
private static final String KEY = ‘d82ee6…’
private final Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(‘http://api.openweathermap.org’😉
.build()
List<Weather> getWeatherList(String city=’Marlborough’, String state=’CT’) {
OpenWeatherMap owm = retrofit.create(OpenWeatherMap)
String address = "${URLEncoder.encode(city, ‘UTF-8’)},$state"
Call<Model> model = owm.getData(q: address, units: ‘imperial’,
cnt: ’16’, APPID: KEY)
model.execute().body().list.collect { WeatherData wd ->
Weather.parseData(wd)
}
}
}
[/source]
I made both attributes private
and final
because I didn’t want Groovy to auto-generate and getters or setters for them. The instance of Retrofit
is created using a builder, with its fluent syntax, in the recommended manner.
The getWeatherList
method takes two strings representing the city and state. I gave both defaults (cool that you can do that in Groovy, isn’t it?), so I can invoke this method with zero, one, or two arguments, as the test cases will show.
The next requirement for Retrofit is that you provide an interface with the methods you want to invoke. In this case I called it OpenWeatherMap
:
[code language=”groovy”]
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.QueryMap;
import java.util.Map;
public interface OpenWeatherMap {
@GET("data/2.5/forecast/daily")
Call<Model> getData(@QueryMap Map<String, String> params);
}
[/code]
While I could have written that in Groovy, in this case I provided it in Java, just to make the integration cleaner. The GET
annotation shows that relative to the base URL I need to access the given path, and the QueryMap
annotation is applied to a map of parameters used to form the resulting query string. The return type is a Call
.
Returning to the getWeatherList
method, I used the create
method on retrofit
to return an implementation of OpenWeatherMap
. Then to make the actual call, I need to invoke the execute
method using my map of parameters. Groovy makes that part particularly easy:
[code language=”groovy”]
Call<Model> model = owm.getData(q: address, units: ‘imperial’, cnt: ’16’, APPID: KEY)
[/code]
That uses the normal Groovy native syntax for maps. You’ll note that I URL encoded the city when assembling the address, using the normal (Java) URLEncoder
class in the standard library.
Once I executed the call, I traversed to the list
child element, based on the attribute name used in the JSON response. That gave me my collection of WeatherData
objects.
Then I needed to map the WeatherData
class to my desired Weather
class, which I did through a static
method called parseData
in Weather
.
[code language=”groovy”]
import groovy.transform.ToString
import java.text.NumberFormat
@ToString
class Weather {
final static NumberFormat numberFormat = NumberFormat.instance
final static NumberFormat percentFormat = NumberFormat.percentInstance
String day
String min
String max
String humidity
String description
URL iconURL
static Weather parseData(WeatherData data) {
numberFormat.setMaximumFractionDigits(2)
new Weather(day: new Date(data.dt * 1000).format(‘EEEE’),
min: numberFormat.format(data.temp.min) + ‘\u00B0F’,
max: numberFormat.format(data.temp.max) + ‘\u00B0F’,
humidity: percentFormat.format(data.humidity / 100),
description: data.weather[0].description,
iconURL: "http://openweathermap.org/img/w/${data.weather[0].icon}.png".toURL()
)
}
}
[/code]
That (almost) matches the Java Weather
POJO in the book, which I populated from the WeatherData
values. The last line in the getWeatherList
method:
[code language=”groovy”]
model.execute().body().list.collect { WeatherData wd ->
Weather.parseData(wd)
}
[/code]
converts the array of WeatherData
objects into a collection of Weather
objects and returns it.
To make sure this is working, here’s my test case:
[code language=”java”]
import org.junit.Test;
import java.util.List;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.*;
public class DownloadForecastTest {
private DownloadForecast df = new DownloadForecast();
@Test // default city,state is Marlborough,CT
public void getWeatherList_MarlboroughCT() throws Exception {
List<Weather> weatherList = df.getWeatherList();
assertThat(16, equalTo(weatherList.size()));
System.out.println("Today’s weather: " + weatherList.get(0));
}
@Test // specify just city defaults to state of CT
public void getWeatherList_NewLondonCT() throws Exception {
List<Weather> weatherList = df.getWeatherList("New London");
assertThat(16, equalTo(weatherList.size()));
System.out.println("Today’s weather: " + weatherList.get(0));
}
@Test // the weather has got to be better in Honolulu
public void getWeatherList_HonoluluHI() throws Exception {
List<Weather> weatherList = df.getWeatherList("Honolulu", "HI");
assertThat(16, equalTo(weatherList.size()));
System.out.println("Today’s weather: " + weatherList.get(0)); }
}
[/code]
I used Java to write the test, mostly to demonstrate that I can access the Groovy classes from Java without any issues. All I’m testing is that I get 16 Weather
objects in the results, as I expected (because of the supplied value of the cnt
parameter). The printed output shows today’s weather in each location.
[code language=”groovy”]
Today’s weather: Weather(Thursday, 77.38°F, 79.36°F, 97%, scattered clouds, http://openweathermap.org/img/w/03n.png)
Today’s weather: Weather(Thursday, 46.44°F, 48°F, 90%, moderate rain, http://openweathermap.org/img/w/10d.png)
Today’s weather: Weather(Thursday, 49.69°F, 51.48°F, 97%, moderate rain, http://openweathermap.org/img/w/10d.png)
[/code]
The first result is for Honolulu; the other two are in Connecticut. In other words, April hasn’t really made it’s way to Connecticut yet.
Now that the system is working, the next step would be to port everything to Java and add it to the Android app, making the REST call in an AsyncTask
and so on. After coding in Groovy, however, the idea of porting all that easy code back into Java is just depressing, so I decided to blog about it instead.
Leave a Reply