Understanding MVVM on Android Tutorial 03 – Networking with Retrofit2

Now that we have defined our model, the next step is define our network calls. We will be using the popular Retrofit2 library to handle the networking calls for our app. Retrofit2 is another great product from Square Open Source ( note that Retrofit2 uses OKHttp3 to make the actual network calls).

To get started with using Retrofit2, we will add the following 2 lines to the app’s build.gradle file. The first line pulls in the Retrofit2 library and the second line is there because we are using GSON to parse the returned data in our app.

compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

There are a number of other converters available with Retrofit2. For example, if you were interested in using Jackson or XML to parse your data, the following libraries are also available:

compile 'com.squareup.retrofit2:converter-jackson:2.1.0'
compile 'com.squareup.retrofit2:converter-simplexml:2.1.0'

The second step is to add the permission to access the internet to your manifest file.

<uses-permission android:name="android.permission.INTERNET">

Then we will define our interface for accessing the GitHub API.

package com.kyubid.sample.mvvmtutorial.networking;

import com.kyubid.sample.mvvmtutorial.model.Repo;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface GithubService {
    String ENDPOINT = "https://api.github.com";

    @GET("/users/{user}/repos")
    Call<List<Repo>> reposForUser (@Path("user") String user);
}

In our Retrofit Interface class above, we have done the following:
1. Defined the base url for our api : “https://api.github.com”
2. Remember that our target endpoint is : “https://api.github.com/users/square/repos”. So we will use a GET request to obtain the JSON data with the remaining half of the request “/users/{user}/repos”. Together with the base url, it forms the final request: “https://api.github.com/users/{user}/repos”
3. At runtime, we will pass in the {user} parameter using (@Path(“user”) String user).
4. We will store the JSON response in a List of Repo objects.

In some developer code samples, you will see that the Retrofit interface class is labelled as something-API. I feel this is incorrect, as an API refers to the complete set of endpoints exposed by the host for communicating with its particular software. Typically an app just calls a subset of the endpoints in an API. So I tend to call my classes, something-Service. Other developers may refer to these same classes as something-Client. Whatever you choose, just be consistent throughout your app in how you name your networking interfaces.

Finally, we will make the network call in our main activity

public class MainActivity extends AppCompatActivity {

    String TAG = "MainActivity";

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

        Gson gson = new GsonBuilder()
                .create();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(GithubService.ENDPOINT)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
        
        GithubService githubClient = retrofit.create(GithubService.class);

        // Fetch a list of the Github repositories.
        Call<List<Repo>> call = githubClient.reposForUser("square");

        // Execute the call asynchronously.
        call.enqueue(new Callback<List<Repo>>() {
        
          @Override
          public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
                if (response.isSuccessful()) {
                   List repos = response.body();
                   for (Repo repo: repos) {
                      Log.d(TAG, "Repo is: " + repo.toString());
                   }
                 } else {
                    //errors like 404s show up here, so assume nothing...nothing I tell you!
                    //infact all the error codes show up here : status 400 -599
                    //so even if onResponse called, it could still be null
                    //you can find out exact error with : response.errorBody().string()
               }
            }

          @Override
          public void onFailure(Call<List<Repo>> call, Throwable t) {
                // the network call was a failure
                Log.d(TAG, "Error Occurred");
            }
        });

    }
}

And that is all we need to do to return a correctly parsed list of Repos using the GitHub API.

——-

Let’s have a look at each pf the components in our networking call in turn. In our app, we are using a very simple GsonBuilder, but there are a number of things we could have done to customise the GsonBuilder to our needs. Some of the most commonly used configurations are:

Gson gson = new GsonBuilder()
    //serialize Date objects according to the style value provided
    .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") 
    /*apply a specific naming policy to an object's field during serialization and 
    deserialization.*/
    .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
    //configures Gson for custom serialization or deserialization.
    .registerTypeAdapter(Type type, Object typeAdapter)
    .create();

You can see the full options available here.

Moving on to the RetrofitBuilder, we see that it is also quite simple. We defined our base url (calling the endpoint we specified in the GithubService class) and we declared we are using a GsonConverterFactory. Then we created an implementation of our GithubService interface using the RetrofitBuilder and we were good to go.

Could we have extended the functionality of the RetrofitBuilder like we did for the GsonBuilder? Yes, there are a number of things we could do to extend the functionality of the RetrofitBuilder. For example, we could do things like certificate pinning or adding Authorization keys. See below for an example of how to add http logging to an app by adding a HttpLoggingInterceptor to the OkHttpClient. I’m not doing that for this app and if you ever use HTTP logging interceptors, please remember to strip them out of your app before they go to production. See the example code below:

        HttpLoggingInterceptor httpLoggingInterceptor 
                                            = new HttpLoggingInterceptor();

        // Can be Level.BASIC, Level.HEADERS, or Level.BODY
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
        if (BuildConfig.DEBUG)
        {
            okHttpClientBuilder.addInterceptor(httpLoggingInterceptor);
        }
        OkHttpClient okHttpClient = okHttpClientBuilder.build();

       //pass this custom OkHttpClient into the Retrofit builder:
       Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(GithubService.ENDPOINT)
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

Finally we made our network call and this is where things can get a bit interesting in Retrofit2. From the last post, we saw our JSON returns an array of Repo objects. So in our response, we are expecting a list of repos. Notice that we explicitly state the type of response object we are expecting in our call.

Call<List<Repo>> call = githubClient.reposForUser("square");

And in all our callback methods, we again explicitly state the type of response object we are expecting.

     
          //See here
          call.enqueue(new Callback<List<Repo>>() {
        
          //...and here
          @Override
          public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
                List repos = response.body();
                for (Repo repo: repos) {
                    Log.d(TAG, "Repo is: " + repo.toString());
                }
            }

          //...also here
          @Override
          public void onFailure(Call<List<Repo>> call, Throwable t) {
                // the network call was a failure
                Log.d(TAG, "Error Occurred");
            }
        });

Let us compare our JSON response from the GitHub API vs a JSON response from the StackOverflow API using the endpoint url: https://api.stackexchange.com/2.2/questions

 

We can see that while the GitHub query returns a “list” of repositories, the StackOverflow query returns a JSON object (“items”) which contains a list of questions.

Therefore we will make a change to how we define our interface for accessing the StackOverflow API compared to the GitHub API.

import com.kyubid.sample.mvvmtutorial.model.QuestionListing;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface StackoverflowService {
    String ENDPOINT = "https://api.stackexchange.com";

    @GET("/2.2/questions?order=desc&sort=creation&site=stackoverflow")
    Call<QuestionListing> loadQuestions(@Query("tagged") String tags);
}

The QuestionListing Class

    public class QuestionListing {
       //Our main object is not a list but it contains a list
       public List<Question> items;//notice that the name "items" matches the JSON

       public List getQuestions() {
         return items;
       }
    }

The Question Class

    public class Question {
       String title;
       String link;

       @Override
       public String toString() {
          return(title);
       }
   }

And this is how we will make the network call in our main activity

public class MainActivity extends AppCompatActivity {

    String TAG = "MainActivity";

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

        Gson gson = new GsonBuilder()
                .create();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(StackoverflowService.ENDPOINT)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
        StackoverflowService client = retrofit.create(StackoverflowService.class);

        // Fetch the object containing the list of the questions.
        Call<QuestionListing> call = client.loadQuestions("android");

        // Execute the call asynchronously.
        call.enqueue(new Callback<QuestionListing>() {
            @Override
            public void onResponse(Call<QuestionListing> call, Response<QuestionListing> response) {
                if (response.isSuccessful()) {
                   QuestionListing questionListing  = response.body();
                   List<Question> questions = questionListing.getQuestions();
                   for (Question question: questions) {
                      Log.d(TAG, "Question is: " + question.toString());
                   }
                } else {
                      Log.d(TAG, "Response not found");
               }
            }

            @Override
            public void onFailure(Call<QuestionListing> call, Throwable t) {
                // the network call was a failure
                Log.d(TAG, "Error Occurred");
            }
        });

    }
}

Keep a note of the differences above and you will hopefully avoid the errors that can occur while defining your Retrofit2 network calls for various APIs.

Further Reading and Comments:
In some instances, you may want to define the full Url in your calls. You can achieve this by specifying the full path (the Base Url set on RetrofitBuilder will be ignored/overridden). Here is an example below using our GitHubService class.

package com.kyubid.sample.mvvmtutorial.networking;

import com.kyubid.sample.mvvmtutorial.model.Repo;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface GithubService {

    @GET("https://api.github.com/users/square/repos")
    Call<List<Repo>> reposForUser ();
}

You can find information on the Retrofit2 library here

Upcoming posts in the series:
Understanding MVVM on Android Tutorial 04 – Creating the View with RecyclerViews, CardViews and Picasso
Understanding MVVM on Android Tutorial 05 – Creating the ViewModel

Leave a Reply