Retrofit is one great lib to handle REST API calls in Android apps. Its 2.0 beta 1 version came out not too long ago, and I had to implement a small project which used it, so I figured “why not show the world my breakthroughs?” (Yeah, right). Here it goes.
Before starting, make sure you add these dependencies to your gradle.build file:
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
For this tutorial (is it a tutorial, though?) I’ll use OpenWeatherMap’s cool API, which is very flexible and allows you to access the current weather information for any place in the world.
The first thing we’ll need to create is a model class, so we can map the response to something and access its values easily. Let’s call it WeatherInfo
, as it will hold the the information for a given place. But wait! We don’t know yet what a response will look like. If we issue a GET request to api.openweathermap.org/data/2.5/weather?q=London, we get something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"coord": { | |
"lon": -0.13, | |
"lat": 51.51 | |
}, | |
"weather": [ | |
{ | |
"id": 800, | |
"main": "Clear", | |
"description": "Sky is Clear", | |
"icon": "01d" | |
} | |
], | |
"base": "cmc stations", | |
"main": { | |
"temp": 290.12, | |
"pressure": 1028, | |
"humidity": 45, | |
"temp_min": 289.15, | |
"temp_max": 291.48 | |
}, | |
"wind": { | |
"speed": 3.1, | |
"deg": 90 | |
}, | |
"clouds": { | |
"all": 0 | |
}, | |
"dt": 1443282691, | |
"sys": { | |
"type": 1, | |
"id": 5091, | |
"message": 0.0036, | |
"country": "GB", | |
"sunrise": 1443246807, | |
"sunset": 1443289743 | |
}, | |
"id": 2643743, | |
"name": "London", | |
"cod": 200 | |
} |
To keep it simple, let’s focus only on the weather
and name
fields of the response. Our WeatherInfo
POJO will look like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mycompany.myfirstapp.model.weather; | |
public class WeatherInfo { | |
private Weather[] weather; | |
private String name; | |
public Weather[] getWeather() { | |
return weather; | |
} | |
public void setWeather(Weather[] weather) { | |
this.weather = weather; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
} |
As you might have noticed, weather
is actually an array of objects, so we have to create a Weather
class, which in turn holds a description
attribute. Here it is!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mycompany.myfirstapp.model.weather; | |
public class Weather { | |
private String description; | |
public String getDescription() { | |
return description; | |
} | |
public void setDescription(String description) { | |
this.description = description; | |
} | |
} |
Now that our model structure is complete, let’s move to our second step.
The next thing we’ll need is an interface declaring the methods we’ll use. These methods must return a Call
instance, which will be useful later, when we handle the actual request.
Let’s say we want to make 2 types of requests: one to get a place’s weather by its name, and another to get it with its coordinates. This is the way we’d do it with Retrofit:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mycompany.myfirstapp.api; | |
import com.mycompany.myfirstapp.model.weather.WeatherInfo; | |
import retrofit.Call; | |
import retrofit.http.GET; | |
import retrofit.http.Query; | |
public interface WeatherInfoApi { | |
@GET("/data/2.5/weather") | |
Call<WeatherInfo> getWeatherForName(@Query("q") String name); | |
@GET("/data/2.5/weather") | |
Call<WeatherInfo> getWeatherForCoords(@Query("lat") double lat, @Query("lon") double lon); | |
} |
Notice we don’t actually implement the methods, just declare them. Retrofit will use the URL provided in the @GET
annotation, and the parameters specified using @Query
to actually implement the methods’ logic.
Now, let’s define a service, which will handle Retrofit’s configuration and will expose an API for other components (like Activities) to use. It’s actually a very simple class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mycompany.myfirstapp.service; | |
import com.mycompany.myfirstapp.api.WeatherInfoApi; | |
import java.io.IOException; | |
import retrofit.Callback; | |
import retrofit.GsonConverterFactory; | |
import retrofit.Retrofit; | |
public class WeatherInfoService { | |
private WeatherInfoApi weatherInfoApi; | |
public WeatherInfoService() { | |
Retrofit retrofit = new Retrofit.Builder() | |
.baseUrl("http://api.openweathermap.org") | |
.addConverterFactory(GsonConverterFactory.create()) | |
.build(); | |
weatherInfoApi = retrofit.create(WeatherInfoApi.class); | |
} | |
public void getWeatherForName(String name, Callback callback) throws IOException { | |
weatherInfoApi.getWeatherForName(name).enqueue(callback); | |
} | |
public void getWeatherForCoords(double lat, double lon, Callback callback) throws IOException { | |
weatherInfoApi.getWeatherForCoords(lat, lon).enqueue(callback); | |
} | |
} |
The constructor takes care of configuring Retrofit, by:
- Setting the requests’ base URL (It really has to be the base URL. Believe me. It will remove all trailing paths, like /data/2.0).
- Setting the library it’ll use to convert the responses’ body into our model structure (This is why we had to include Retrofit’s GSON converter in our dependencies). Another popular choice is Jackson, but GSON is just fine for us.
This is where Retrofit’s magic happens. Once it’s configured, we can create an instance of our WeatherInfoApi
interface, allowing us to use its methods and make requests to the endpoints we set up.
Notice the methods receive a Callback
as a parameter. This will allow the services and activities which use our service to specify tasks they want to be executed when the response arrives. They are queued to be executed asynchronously, so that the calling thread doesn’t get blocked, avoiding causing a weird experience for the user. We’ll see this more clearly next.
We’ll create an Activity
that uses our service’s methods to get the weather with a place’s name or coordinates. The name string and coordinates can be obtained in various ways, like text fields or a map (a cooler way, IMHO). I won’t go into that part, though, so I’ll leave it up to your imagination! Take a look at our Activity’s class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mycompany.myfirstapp.activity.weather; | |
import android.content.Intent; | |
import android.support.v7.app.AppCompatActivity; | |
import android.os.Bundle; | |
import java.io.IOException; | |
import com.mycompany.myfirstapp.service.WeatherInfoService; | |
import com.mycompany.myfirstapp.model.weather.WeatherInfo; | |
import retrofit.Callback; | |
import retrofit.Response; | |
public class WeatherActivity extends AppCompatActivity { | |
// This will hold our WeatherInfoService instance. | |
private WeatherInfoService weatherInfoService; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
// onCreate logic… | |
// Initialize our service instance. | |
weatherInfoService = new WeatherInfoService(); | |
} | |
private void getWeatherForName(String name) { | |
try { | |
weatherInfoService.getWeatherForName(name, new Callback() { | |
// This is executed when the request goes well 🙂 | |
@Override | |
public void onResponse(Response response) { | |
// response.body holds the response, mapped to our model. | |
WeatherInfo weatherInfo = (WeatherInfo) response.body(); | |
// Do something with weatherInfo, like | |
// String description = weatherInfo.getWeather()[0].getDescription(); | |
// which will be something like "Sky is Clear". | |
} | |
// This one's called when it goes bad 😦 | |
@Override | |
public void onFailure(Throwable t) { | |
// Tell the user there was an error. | |
} | |
}); | |
} catch (IOException e) { | |
// Recover from failure. | |
} | |
} | |
private void getWeatherForCoords(double lat, double lon) { | |
try { | |
weatherInfoService.getWeatherForCoords(lat, lon, new Callback() { | |
@Override | |
public void onResponse(Response response) { | |
// here, too, response.body holds the response, mapped to our model. | |
WeatherInfo weatherInfo = (WeatherInfo) response.body(); | |
// Do something with it. Or not. But that would be weird. | |
} | |
@Override | |
public void onFailure(Throwable t) { | |
// Tell the user something went wrong. | |
} | |
}); | |
} catch (IOException e) { | |
// Recover from failure! | |
} | |
} | |
} |
If you’ve ever done something in front-end Javascript, you’ll find these Callbacks are actually very much like promises, and they allow the Activity to handle how it will react if the requests go well or bad. Retrofit uses the GSON converter to map the response’s body to the model we defined, so now we can use its values however we want!
I hope this helps while Retrofit updates their documentation (they erased their 1.x docs and uploaded unfinished ones for 2.x -__-). If you have any questions or suggestions, I’d be happy to hear them!