Monday, February 1, 2016

Network calls using Retrofit on Android

The purpose of this tutorial is to teach you how to setup Retrofit to make a network call on a clean, brand new Android application.
Retrofit works great with JSON and XML data, but the setup for JSON and XML is different in the deserializer, so in this tutorial I will stop once you get the data from the callback. In a later tutorial I will show how to deserialize (parse) the JSON or XML data received.

Note: this tutorial uses Android Studio 1.5.9 and Retrofit 1.9.0. For this tutorial we will get sample weather data in JSON format from open weather map. 

Sample URL and Sample JSON

We will use weather data from open weather map for this sample, so you might want to create a free account there to get a token. Their site is http://openweathermap.org/
We will be getting their Call for several cities ID network call, which returns a JSON like this:
{
  "cnt": 1,
  "list": [{
    "coord": {
      "lon": -0.13,
      "lat": 51.51
    },
    "sys": {
      "type": 1,
      "id": 5091,
      "message": 0.0048,
      "country": "GB",
      "sunrise": 1454226003,
      "sunset": 1454258868
    },
    "weather": [{
      "id": 803,
      "main": "Clouds",
      "description": "broken clouds",
      "icon": "04n"
    }],
    "main": {
      "temp": 12.47,
      "pressure": 1011,
      "humidity": 82,
      "temp_min": 11.8,
      "temp_max": 13
    },
    "wind": {
      "speed": 9.3,
      "deg": 250
    },
    "clouds": {
      "all": 75
    },
    "dt": 1454277230,
    "id": 2643743,
    "name": "London"
  }]
}
When it's all said and done, this should be the JSON we want to download and parse.

Workflow

Because using Retrofit requires a setup class and an interface, I created a super awesome looking workflow to hopefully explain this better:

So in the most basic scenario, you'll need at least 2 classes. Ideally you would set this up in at least 3 classes:
  1. An activity or class that triggers the call. This is usually an Android activity that starts the process after a button is pressed, or some other user interaction occurs
  2. A service class. While this could be in the same class as the activity, the idea of Retrofit is to reuse some elements to make multiple calls. This class sets up the RequestInterceptor, RestAdapter and deserializer. After that this calls an Interface to make the actual HTTP call
  3. The Service interface. This is an interface with just the url, parameters and type of call to make (GET, PUT, POST...). In here we receive the callback and we will update it so the activity that triggered this process (element #1 in this list) gets the data in an asynchronous way.
All of this happens in a asynchronous way on a separate thread, so you can (and will) call this process from the main UI thread without having to worry about creating or managing threads.

Now that we know the workflow we will use, let's get working from the Service Interface!

Setup

Before we start using Retrofit, we need to get the Retrofit SDK into our application.
Open your build.gradle file, and go to the dependencies section.
In here, we will add retrofit, okhttp and the gson libraries, like this:
    compile 'com.squareup.retrofit:retrofit:1.9.0'
    compile 'com.squareup.okhttp:okhttp:2.5.0'
    compile 'com.google.code.gson:gson:2.4'
So now, in a simple brand new android application, the whole gradle file would look like this:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "eduardoflores.com.test_networkconnection"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
    compile 'com.squareup.retrofit:retrofit:1.9.0'
    compile 'com.squareup.okhttp:okhttp:2.5.0'
    compile 'com.google.code.gson:gson:2.4'
}
As usual, you may have additional information here, but we what we really care about for this tutorial are the lines for retrofit 1.9.0, okhttp 2.5.0 and gson 2.4.

Now, sync your gradle file to get the new SDK.

Update your manifest file

Since we're making a network call, and somehow Android still requires a permission for Internet in 2016, we need to add the permission for internet to the manifest file.
So, open the manifest file and add the Internet permission:
<uses-permission android:name="android.permission.INTERNET"/>

Create a basic return method

I know I said we will leave JSON and XML parsing for another time, and I will get much more specific on a different tutorial, but retrofit requires at least 1 object type to return in the callback.
Sure, we could use "Object" but let's do things right.

Create a new Java class named WeatherData. This WeatherData.java file will, for now, only contain 1 field for count.
Since the JSON key we will be getting for count is actually ctn, and I don't want to use that non-obvious name, we need to use an annotation to convert ctn to count. In the end, our entire WeatherData.java class looks like this:
package eduardoflores.com.test_networkconnection;

import com.google.gson.annotations.SerializedName;

/**
 * @author Eduardo Flores
 */
public class WeatherData {

    @SerializedName("cnt")
    public int count;

}


Understanding your HTTP request

If you know what GET calls are, you might want to skip this part.
 
Like mentioned before, we are going to get sample weather data in JSON form from Open Weather Map. This is their full url (although you need your own unique token):

http://api.openweathermap.org/data/2.5/group?id=524901,703448,2643743&units=metric&appid=44db6a862fba0b067b1930da0d769e98

Before moving forward, let's understand what this url is.
  • This is a GET call with multiple parameters
  • The host url is just http://api.openweathermap.org (you can create a free account here to get a similar JSON)
  • The path is "/data/2.5/group"
  • The first GET parameter is "id" with a value of "524901,703448,2643743"
  • The second GET parameter is "units" with a value of "metric"
  • The third, and last, GET parameter is "appid" with a value of "44db6a862fba0b067b1930da0d769e98"
This appid parameter is your token. You need to get a new one for this to work.

Setup the Service Interface

So we're going to break our URL into at least 2 parts, the host, and whatever else is in the url, starting with the slash "/".

We will create a new call (interface) named ServicesDownloader, and in here we will create a method call named getWeatherData with multiple parameters for the id, units, appid, and the callback of WeatherData type.
The entire ServicesDownloader.java interface looks like this:
package eduardoflores.com.test_networkconnection;

import retrofit.Callback;
import retrofit.http.GET;
import retrofit.http.Query;

/**
 * @author Eduardo Flores
 */
public interface ServicesDownloader
{
    @GET("/data/2.5/group")
    void getWeatherData(@Query("id")String id,
            @Query("units")String units,
            @Query("appid")String appid,
            Callback callback);
}


Retrofit Annotations

WHAT THE @&#* ARE THOSE @GET AND @QUERY THINGS??!!!!

Let me explain this. Retrofit uses these things called annotations, and these annotations do a lot of the heavy lifting for us in a simple word or line.
Here are some of the most commonly used annotations with Retrofit:

@GET("/someURL")
@POST("/someURL")
@PUT("/someURL")

These 3 make a GET, POST or PUT HTTP calls. The URL begin with the / after the domain.

@Query("queryName") String param
@Path("pathName") String param
@QueryMap("keyValuePair") Map <String, String> param
@Body("requestBody") String bodyOfRequest

The @Query annotation is used for adding elements to the URL GET call. For example, if the GET request uses a url like this:

http://www.example.com/someGETrequest?param1=data1&param2=data2

Then our Retrofit call would be:

@GET("/someGETrequest")
void someJavaMethodName(@Query("param1") String myData1, @Query("param2") String myData2;

Notice how we don't need to enter ?, & or = symbols. Retrofit does this for us.

@QueryMap and @Body are used the same way.

The @Path annotation is used for when we need to place something in the url, like a variable.
For example, if our URL GET call is:

http://www.example.com/en_US/someGETrequest

The en_US will be a locale, and this will vary depending on the locale we want. So for this, we would format our url like this:

@GET("/{locale}/someGETrequest")
void someJavaMethod(@Path("locale") String someLocale);

You can now mix and match them. For additional information, you may want to refer to the retrofit documentation.

Create the Service class

With the Services interface all finished, we now need to work on the next piece of the workflow, which is the Service java class.
Create a new Java class, and name it ServiceDownloader.java

In the new ServiceDownloader class, create a new private variable that refers to our previously created Interface:
private final ServicesDownloader servicesDownloader;

Create a constructor

Next we will create a constructor for the ServiceDownloader class, with a parameter of heades (even though we're not really using them in this tutorial)
public ServiceDownloader(final Map headers)
{
    RequestInterceptor requestInterceptor = new RequestInterceptor() {
        @Override
        public void intercept(RequestFacade request) {
            // handle the headers
            if ( !headers.isEmpty())
            {
                for (Map.Entry entry: headers.entrySet())
                {
                    request.addHeader(entry.getKey(), entry.getValue());
                }
            }
        }
    };

    // Get GSON
    Gson gson = new GsonBuilder().create();

    OkHttpClient client = new OkHttpClient();
    client.setReadTimeout(2, TimeUnit.MINUTES);

    // setting up the log level
    RestAdapter.LogLevel logLevel = RestAdapter.LogLevel.FULL;

    // create the rest adapter
    RestAdapter restAdapter = new RestAdapter.Builder()
            .setLogLevel(logLevel)
            .setEndpoint("http://api.openweathermap.org")
            .setRequestInterceptor(requestInterceptor)
            .setClient(new OkClient(client))
            .setConverter(new GsonConverter(gson))
            .build();

    servicesDownloader = restAdapter.create(ServicesDownloader.class);
}
In here we are doing the following:
  • Create a RequestInterceptor, and add the headers (if there are any)
  • Create a new GSON serializer. You can go to town with this, but a basic GsonBuilder will work for 99% of your requests
  • Setup a new OkHttpClient, and set a timeout. I set mine at 2 mins
  • You can set a log level. For debugging purposes full debug is my preference
  • Create the RestAdapter. In here add the log level you created, the RequestInterceptor, the okHttpClient, the Gson converter (could've been xml), and the "end point"
  • Add the newly created RestAdapter to your servicesDownloader variable.
As you can see here, the setEndPoint variable has a url of ("http://api.openweathermap.org"). This is the domain of our url. You can even set this in the build gradle file if you want, but the important part of this is to understand that retrofit uses the concept of "domain + something else". In here we set the domain part.

Create a method

With the constructor done, we need to create a method to actually call the ServicesDownloader interface, but with the setup you added in the constructor (that's why it's a variable)
I created this method:
public void getWeatherData(Callback callback)
{
    String id = "524901,703448,2643743";
    String units = "metric";
    String appid = "44db6a862fba0b067b1930da0d769e98";
    servicesDownloader.getWeatherData(id, units, appid, callback);
}
This is a super straight forward Java method. The id, units and maybe even appid variables could come from the calling activity, but for the purpose of easy reading I decided to place them here.

That's all. We're done with the gradle file, the manifest, the interface and the service class
Now all we have to do is finish the calling activity.

Modify the Activity

In the activity (MainActivity.java for me) we have to do 2 things for sure, and one optional:
  1. Call the getWeatherData method in the ServiceDownloader java class with a callback
  2. Create a callback, and handle the success or failure scenarios
  3. Create the http headers (optional)

Create the http headers 

This is not required for this tutorial, but odds are you're gonna have to add the headers to your real call, so might as well add them. These headers are basic and won't make or break anything, but the concept and structure would be demostrated.
public static Map getRequestHeaders() {
    Map headers = new HashMap<>();
    headers.put("Accept", "application/json");
    headers.put("Content-Type", "application/json");
    return headers;
}

Calling the getWeatherData method

In the onCreate method of the activity we then add this:
ServiceDownloader serviceDownloader = new ServiceDownloader(getRequestHeaders());
serviceDownloader.getWeatherData(weatherCallback);
And as you can see, I'm missing the variable weatherCallback. Let's make it!

Make the callback


In the activity (in the class as a variable, outside of any method), create a new callback variable, like this:
public Callback<Weatherdata> weatherCallback = new Callback<Weatherdata>() {
    @Override
    public void success(WeatherData weatherQuery, Response response) {
        Log.i("MY_APP", "count = " + weatherQuery.count);
    }

    @Override
    public void failure(RetrofitError error) {
        Log.e("MY_APP", error.getLocalizedMessage());
    }
};

This is a variable of type Callback which takes a type of what we're expecting back from the network. In our case we will use theWeatherData type we created at the begining of the tutorial (the one with just 1 field of count) because this is the type we're expecting.
In the end, the whole MainActivity.java looks like this:
package eduardoflores.com.test_networkconnection;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;

import java.util.HashMap;
import java.util.Map;

import retrofit.Callback;
import retrofit.RetrofitError;
import retrofit.client.Response;

public class MainActivity extends AppCompatActivity {

    public Callback<WeatherData> weatherCallback = new Callback<WeatherData>() {
        @Override
        public void success(WeatherData weatherQuery, Response response) {
            Log.i("MY_APP", "count = " + weatherQuery.count);
        }

        @Override
        public void failure(RetrofitError error) {
            Log.e("MY_APP", error.getLocalizedMessage());
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ServiceDownloader serviceDownloader = new ServiceDownloader(getRequestHeaders());
        serviceDownloader.getWeatherData(weatherCallback);
    }

    public static Map<String string> getRequestHeaders() {
        Map<string string=""> headers = new HashMap<>();
        headers.put("Accept", "application/json");
        headers.put("Content-Type", "application/json");
        return headers;
    }
}

Finally, run the app and you should see an output in the console of count = 3. (Why 3? because we passed 3 groups of cities as the 'id' parameter to the service)

You can also see in the console a lot of output with the tag Retrofit. This means the downloader is working, and we can so far parse the element ctn in the root.

Yay we did it!

Now's time to learn how to deserialize the JSON data received.

4 comments:

  1. "Great blog created by you. I read your blog, its best and useful information. You have done a great work. Super blogging and keep it up.php jobs in hyderabad.
    "

    ReplyDelete
  2. Great blog you have written for us Thanks for sharing good content keep sharing... check it once through MSBI Online Training for more information on microsoft.

    ReplyDelete
  3. • Nice and good article. It is very useful for me to learn and understand easily. Thanks for sharing your valuable information and time. Please keep updatingAzure Online Training

    ReplyDelete