Friday, March 25, 2016

Network calls using Retrofit 2.0

Hey look!
And with that, I'll make a new entry showing how to use Retrofit 2.0!

Note 1: this tutorial will use GSON as our deserializer. I've already written a GSON tutorial, so if you need help understanding GSON, check out what I did here.

Note 2: I have already written a tutorial on retrofit 1 in case you need to work with that instead. I refer the tutorial for Retrofit 1 a few times.

Create a new project

I'll assume that by now you know how to create a project. Alternatively you can apply this to an existing project, but for clarity I'll do this tutorial on a new blank project.

Add dependencies

Go to the build.gradle, and add the following dependencies:
compile 'com.squareup.retrofit2:retrofit:2.0.0'
compile 'com.squareup.retrofit2:converter-gson:2.0.0'
compile 'com.google.code.gson:gson:2.6.2'
We will be using the release version of retrofit 2.0, along with gson and the gson converter for retrofit 2.0.

So with those dependencies in place, this is what my entire gradle.build file looks like:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "eduardoflores.com.test_retrofit2"
        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.squareup.retrofit2:retrofit:2.0.0'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0'
    compile 'com.google.code.gson:gson:2.6.2'
}
Done with the gradle.build file.

Our JSON url

We will be parsing the JSON that comes from this url:
http://api.nestoria.co.uk/api?country=uk&pretty=1&encoding=json&listing_type=buy&action=search_listings&page=1&place_name=london

This will give a long and complex JSON. In case the site goes down in the future, here's a sample of what this looks like.

Create Object models for deserialization

In the JSON we can see the root objects of request and response, but both of these objects are inside a larger JSON object. I will call this wrapping JSON object the ServiceResponse (this key name does not show up in the JSON and I just made it up, but I will reference it in Java)

As convention, you may want to create a new folder/package in your Android studio project to hold only your model objects. In my case, I named this folder/package as 'model'

In Android studio, inside of model, create a new Java class and name it ServiceResponse.java.
In here, we're going to have only 2 objects: a Request object, and a Response object.

My ServiceResponse.java java file now looks like this:
package eduardoflores.com.test_retrofit2.model;

import com.google.gson.annotations.SerializedName;

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

    public Request request;

    public Response response;
}
And we're done with ServiceResponse.java.

Now let's create a Request.java file. This Request.java class is going to handle this portion of the code:
   "request" : {
      "country" : "uk",
      "language" : "en",
      "listing_type" : "buy",
      "location" : "london",
      "num_res" : "20",
      "offset" : 0,
      "output" : "json_xs",
      "page" : "1",
      "pretty" : "1",
      "product_type" : "realestate",
      "property_type" : "property",
      "size_type" : "gross",
      "size_unit" : "m2",
      "sort" : "nestoria_rank"
   }
Because of that, in our Request.java class we should create a property for country, language, listing_type, location, num_res...

My Request.java class now looks like this:

package eduardoflores.com.test_retrofit2.model;

import com.google.gson.annotations.SerializedName;

/**
 * @author Eduardo Flores
 */
public class Request
{
    public String country;

    public String language;

    @SerializedName("listing_type")
    public String listingType;

    public String location;

    @SerializedName("num_res")
    public String numRes;

    public Integer offset;

    @SerializedName("json_xs")
    public String jsonXs;

    public String page;

    public String pretty;

    @SerializedName("product_type")
    public String productType;

    @SerializedName("property_type")
    public String propertyType;

    @SerializedName("size_type")
    public String sizeType;

    @SerializedName("size_unit")
    public String sizeUnit;

    public String sort;
}
Again, if you're lost on the GSON conversion, make sure you review my GSON tutorial.
Also, I have no idea what some of these things are, like "json_xs", "num_res" or "pretty" and I probably will never use them.

Now let's create the Response.java file. This file will target this section of the code:
"response" : {
      "application_response_code" : "110",
      "application_response_text" : "listings returned, location very large",
      "attribution" : {
         "img_height" : 22,
         "img_url" : "http://s.uk.nestoria.nestimg.com/i/realestate/all/all/pbr.png",
         "img_width" : 183,
         "link_to_img" : "http://www.nestoria.com"
      },
      "created_http" : "Fri, 25 Mar 2016 16:47:00 GMT",
      "created_unix" : 1458924420,
      "link_to_url" : "http://www.nestoria.co.uk/london/property/buy/results-20",
      "listings" : [
         {
             // a listing object
         }
       ]
       }
   }
The import part here is to see that we will have an Attribution object, and a list of Listings objects. We will need to create these objects as well.

Here's now my Response.java class:
package eduardoflores.com.test_retrofit2.model;

import com.google.gson.annotations.SerializedName;

import java.util.List;

/**
 * @author Eduardo Flores
 */
public class Response
{
    @SerializedName("application_response_code")
    public String applicationResponseCode;

    @SerializedName("application_response_text")
    public String applicationResponseText;

    public Attribution attribution;

    public List<Listing> listings;
}
Now that you know the drill, here are also my Attribution.java and Listing.java objects.
Attribution.java class:
package eduardoflores.com.test_retrofit2.model;

import com.google.gson.annotations.SerializedName;

/**
 * @author Eduardo Flores
 */
public class Attribution
{
    @SerializedName("img_url")
    public String imageUrl;

    // additional properties...
}

And Listing.java class:
package eduardoflores.com.test_retrofit2.model;

import com.google.gson.annotations.SerializedName;

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

    @SerializedName("datasource_name")
    public String datasourceName;

    public String guid;

    public String title;

    // additional properties...
}
And now we're done with the model objects! that took a while...

Now let's go back to focus on Retrofit 2, which is really what we care about.

Retrofit workflow

Let's remember the Retrofit workflow from v1 we want to continue for v2:


We will break our url apart, and then start from right to left, with the Service Interface.

Break URL into parts

You should notice by now that we're making a GET call.
Our entire url is this:
http://api.nestoria.co.uk/api?country=uk&pretty=1&encoding=json&listing_type=buy&action=search_listings&page=1&place_name=london

Our base url is: http://api.nestoria.co.uk

Our interface url is:  api

Our url parameters are: country=uk&pretty=1&encoding=json&listing_type=buy&action=search_listings&page=1&place_name=london

In retrofit 2 the interface URL no longer needs to start with "/" but the code should work with it too. There are discussions on whether it is better to have the URL with and without the "/". For now I will keep it.

Create Retrofit Service Interface

Create a new interface file named Services.java and add the interface portion of the URL:
package eduardoflores.com.test_retrofit2;

import java.util.Map;

import eduardoflores.com.test_retrofit2.model.ServiceResponse;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.QueryMap;

/**
 * @author Eduardo Flores
 */
public interface Services
{
    @GET("/api")
    Call getListings(@QueryMap Map<String,String> parameters);
}
And now you should be asking "What is that Call return type??" and maybe if you were using Retrofit 1 you should be asking "Where's the Callback??"

In Retrofit 2, the return type of the interface method is Call, which allows for your network call to be either synchronous or asynchronous! The specific call type is now determined when we call the method, instead of in the method itself.
And about the missing Callback...well, you no longer need it.

The parameters that I'm passing will be the key-value pairs for the GET call.
If you need more information on what QueryMap is, I've added a more detailed explanation of Retrofit annotations on my Retrofit 1 tutorial.

So that's all you need in the interface class.

Create the Retrofit Service class

Now we need to setup the heart of Retrofit 2.
Create a new java class named Service.java.
We will only use one method in here, so this method will be static, but you could setup the reusable portions of this method into a constructor (like I did on my Retrofit 1 tutorial).

Here's my Service.java class. I'll explain what everything does below the code.
package eduardoflores.com.test_retrofit2;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import eduardoflores.com.test_retrofit2.model.ServiceResponse;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

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

    public static Call getListings(String listingType, String city)
    {
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new Interceptor() {
                    @Override
                    public okhttp3.Response intercept(Chain chain) throws IOException {
                        okhttp3.Response response = chain.proceed(chain.request());
                        System.out.println("request = " + chain.request().url().toString());
                        System.out.println("response = " + response);
                        return response;
                    }
                }).build();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://api.nestoria.co.uk")
                .addConverterFactory(GsonConverterFactory.create())
                .client(client)
                .build();

        Services services = retrofit.create(Services.class);

        Map parameters = new HashMap<>();
        parameters.put("country", "uk");
        parameters.put("pretty", "1");
        parameters.put("encoding", "json");
        parameters.put("listing_type", listingType);
        parameters.put("action", "search_listings");
        parameters.put("page", "1");
        parameters.put("place_name", city);

        return services.getListings(parameters);
    }
}

OkHttpClient: this is was the interceptor was on Retrofit 1. This can serve 2 purposes
1. Here's where you would be adding headers, if you need to add them to network call. These are added the same way as it was on Retrofit 1.
2. This can be used as a debugging tool. Right now I'm outputting the request and the response. This shows me if I'm getting a code 200 from the server, or something else, along with that's the entire call I'm making.
It is worth mentioning that the OkHttpClient is not required.

Retrofit: this used to be the RestAdapter. In here we add the baseUrl, the deserializer adapter, and the client (interceptor)

GsonConverterFactory: this is required in order to use GSON to parse our JSON data, and use the models we created earlier. There are options for XML as well, using Simple-XML.

We then create the Services object (from our Services.java interface) using the retrofit object we just created.

We create the parameters for the GET call as key-value pairs, and then make the call to the getListings() from the interface file.

All of this will return the Call type of object we are expecting from the interface.

Create the Consuming Activity

The time has come to finally consume (use) the data coming from the JSON, and from all of our hard work.
This part is actually pretty simple.

Here's the code for my standard default blank activity MainActivity.java:
package eduardoflores.com.test_retrofit2;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import eduardoflores.com.test_retrofit2.model.Listing;
import eduardoflores.com.test_retrofit2.model.ServiceResponse;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {

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

        Call<ServiceResponse> call = Service.getListings("buy", "london");
        call.enqueue(new Callback<ServiceResponse>() {
            @Override
            public void onResponse(Call<ServiceResponse> call, Response<ServiceResponse> response) {
                ServiceResponse serviceResponse = response.body();

                System.out.println("imageUrl = " + serviceResponse.response.attribution.imageUrl);

                for (Listing listing : serviceResponse.response.listings)
                {
                    System.out.println("listing title = " + listing.title);
                }
            }

            @Override
            public void onFailure(Call<ServiceResponse> call, Throwable t) {
                System.out.println("failure. Throwable = " + t);
            }
        });
    }
}
BUT WHAT DOES IT DOOOOO???!!
Call<ServiceResponse> call = Service.getListings("buy", "london");
This calls our static method getListings(), which returns a Call type, right? That's true, except we are getting a Call object with a deserialized object type ServiceResponse (our root wrapper for the JSON call, remember?)

Then we have 2 choices: synchronous vs asynchronous.
call.enqueue(...)
The enqueue version of the Call object provides us with an asynchronous network call in Retrofit 2. Inside the call.enqueue we can now add a Callback object, and handle the output in whatever way we want.

Alternatively, there's this:
call.execute()
This would execute your network call synchronously.

Additionally you can now also call things like:
call.cancel()
to stop a network call, like when a user returns to the previous activity after a network call has started, but not finished.

So there you have it!
Don't forget about the Internet permission in your manifest, and you should be all set to go with Retrofit 2.0.

3 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 HYDERABAD

    ReplyDelete