time

How to get remote resource with Retrofit in Android

android_retrofit_icon.png

In older architecture of Android networking, if you want to get response from server you should use three libs

  • AsyncTask to do all background task.
  • HttpClient or HttpurlConnection to do networking task.
  • JsonParser class to parse data comes from server.

With Volley or Retrofit you have nothing to do everything is automatic.

Volley VS Retrofit

Android Volley (developed by Google) and Retrofit (developed by Square, Inc) are two of the most used libraries for accessing REST web APIs today. These libraries not only ease the development effort but also, give you many great features like retry mechanism, automatic parsing of data, caching and loading image.

Retrofit is a simple REST client for Android without caching and image loading. What makes it different is that, Retrofit can perform synchronous or asynchronous HTTP with automatic JSON parsing without any effort. Retrofit does the JSON parsing automatically using GSON.

Android Volley can capture four types of responses automatically through these requests: StringRequest, JsonObjectRequest, JsonArrayRequest, ImageRequest. On the other hand Retrofit can parse many other types of responses automatically like: Boolean, Integer, Date, String, Object, Collections.

One of the great things about Volley is that it supports retries on request timeout. Retrofit on the other hand does not have a retry mechanism as of now.

Android Volley library has a very elaborate caching mechanism. Retrofit on the hand, does not support caching.

Volley library has a special type of request to get images from network called ImageRequest. As of now Retrofit does not support the loading of images. You can use Glide or Picasso.

Use Retrofit if your use-case is a standard REST API with JSON responses and not too many custom requirements in terms of caching, request prioritization, retries, etc. Use Volley if you have unusual / fine-grained requirements, or if you anticipate needing a lot of flexibility from your networking layer in the future at the cost of more code. source

Retrofit is powerful library makes it easy to consume JSON or XML data which is then parsed into Plain Old Java Objects (POJOs). GET, POST, PUT, PATCH, and DELETE requests can all be executed.

Retrofit does not have a built-in any JSON converter to parse from JSON to Java objects. Instead it ships support for the following JSON converter libraries to handle that:

  • Gson: com.squareup.retrofit:converter-gson
  • Jackson: com.squareup.retrofit:converter-jackson
  • Moshi: com.squareup.retrofit:converter-moshi

Retrofit supports XML via:

  • Simple Framework: com.squareup.retrofit2:converter-simpleframework

Retrofit tutorial

For this tutorial I will use JSONPlaceholder as data source.

Our plan is

  1. Create a new project using your Android Studio.
  2. Add Retrofit2, GSON, RecylerView and CardView dependencies to build.gradle (Module:app).
  3. Add INTERNET permissions in AndroidManifest.xml file.
  4. Create model class.
  5. Create Retrofit instance.
  6. Define endpoints.
  7. Add RecylerView to our activity_main.xml. Set Recylerview in MainActivity.
  8. Make request.

1. Open your Android Studio and create a new project.

2. Go to GradleScripts > build.gradle (Module:app) and add below libraries to this file under dependencies block. My dependencies block is

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:25.0.1'

    compile 'com.android.support:recyclerview-v7:25.0.1'
    compile 'com.android.support:cardview-v7:25.0.1'

    compile 'com.google.code.gson:gson:2.8.0'
    compile 'com.squareup.retrofit2:retrofit:2.2.0'
    compile 'com.squareup.retrofit2:converter-gson:2.2.0'
}

3. Since we are working with network operations we need to add INTERNET permissions in AndroidManifest.xml file

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.proft.retrofitdemo">

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

    <application>
    ...
    </application>
</manifest>

4. First, we need to know what type of JSON response we will be receiving and then create Java objects that will be able to parse posts. Based on the JSON response returned for this API call, let’s first define how a basic post representation should look like.

The Retrofit GSON converter takes care of converting the JSON objects and store it in our model class. Create a class named PostItem.

import com.google.gson.annotations.SerializedName;

public class PostItem {
    @SerializedName("id")
    private Integer id;
    @SerializedName("userId")
    private Integer userId;
    @SerializedName("title")
    private String title;
    @SerializedName("body")
    private String body;

    public PostItem(Integer id, Integer userId, String title, String body) {
        this.id = id;
        this.userId = userId;
        this.title = title;
        this.body = body;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

The @SerializedName annotation is needed for Gson to map the JSON keys with our fields. In keeping with Java's camelCase naming convention for class member properties, it is not recommended to use underscores to separate words in a variable. @SerializedName helps translate between the two.

5. To send network requests to an API, we need to use the Retrofit Builder class and specify the base URL for the service. So, create a class named ApiClient.

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ApiClient {
    public static final String BASE_URL = "http://jsonplaceholder.typicode.com/";
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

6. The endpoints are defined inside of an interface using special Retrofit annotations to encode details about the parameters and request method. This interface contains methods we are going to use to execute HTTP requests such as GET, POST, PUT, PATCH, and DELETE.

In addition, the return value is always a parameterized Call<T> object such as Call<PostItem>. For instance, ApiInterface interface defines each endpoint in the following way.

import java.util.List;

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

public interface ApiInterface {
    @GET("posts")
    Call<List<PostItem>> getPosts();

    @GET("posts/{id}")
    Call<PostItem> getPostDetails(@Path("id") int id);

    @GET("/posts?order=desc")
    Call<List<PostItem>> getPostsByCategory(@Query("category") String category)
}

Each endpoint specifies an annotation of the HTTP method (GET, POST, etc.) and the parameters of this method can also have special annotations (@Query, @Path, @Body etc.)

Take a look to other annotations

  • @Path - variable substitution for the API endpoint. For example PostItem id will be swapped for {id} in the URL endpoint.
  • @Query – specifies the query key name with the value of the annotated parameter.
  • @Header – specifies the header with the value of the annotated parameter.
  • @Body - payload for the POST call.

Sometimes you want to filter the data from the server. Retrofit has the @Query("key") annotation to use instead of hard-coding it in the endpoint. The key value represents the parameter name in the URL. It will be added to the URL by Retrofit. For example, if we pass the value "linux" as an argument to the getPostsByCategory(String category) method, the full URL will be:

https://blog.com/api/posts?category=linux

7. Let's visualize response. Open your activity_main.xml file and add Recyclerview to it

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rcView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:paddingBottom="16dp"
        android:paddingTop="16dp"
        android:scrollbars="vertical"
         />
</RelativeLayout>

Create a new layout for single row of RecyclerView using CardView. Below is content of file post_item.xml.

<?xml version="1.0" encoding="utf-8"?>

<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:background="#C5CAE9"
    android:foreground="?attr/selectableItemBackground"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    card_view:cardCornerRadius="2dp">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:minHeight="72dp"
        android:orientation="horizontal"
        android:id="@+id/llPosts"
        android:padding="16dp">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tvUserId"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="16dp"
                android:text="UserID"
                android:textColor="@color/colorGrey"
                />
        </LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tvTitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="top"
                android:textStyle="bold"
                android:textSize="16sp"
                android:textColor="@color/colorGrey" />

            <TextView
                android:id="@+id/tvBody"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:maxLines="3"
                android:textColor="@color/colorGreyLight" />
        </LinearLayout>
    </LinearLayout>
</android.support.v7.widget.CardView>

Create a RecylerViewHolder java class for RecyclerView by extending RecyclerView.ViewHolder. Also create an RecylerAdapter java class for RecyclerView by extending RecyclerView.Adapter. More About RecyclerView in Android.

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.List;

public class PostAdapter extends RecyclerView.Adapter<PostAdapter.PostViewHolder> {
    private List<PostItem> posts;
    private int rowLayout;
    private Context context;
    private ItemListener itemListener;

    public class PostViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        LinearLayout llPosts;
        TextView tvUserId;
        TextView tvTitle;
        TextView tvBody;
        ItemListener itemListener;

        public PostViewHolder(View v, ItemListener itemListener) {
            super(v);
            llPosts = (LinearLayout) v.findViewById(R.id.llPosts);
            tvUserId = (TextView) v.findViewById(R.id.tvUserId);
            tvTitle = (TextView) v.findViewById(R.id.tvTitle);
            tvBody = (TextView) v.findViewById(R.id.tvBody);

            this.itemListener = itemListener;
            v.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            PostItem item = getItem(getAdapterPosition());
            this.itemListener.onPostClick(item.getId());

            notifyDataSetChanged();
        }        
    }

    public PostAdapter(List<PostItem> posts, int rowLayout, Context context, ItemListener itemListener) {
        this.posts = posts;
        this.context = context;
        this.rowLayout = rowLayout;
        this.itemListener = itemListener;
    }

    @Override
    public PostAdapter.PostViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(rowLayout, parent, false);
        return new PostViewHolder(view);
    }

    @Override
    public void onBindViewHolder(PostViewHolder holder, final int position) {
        holder.tvTitle.setText(posts.get(position).getTitle());
        holder.tvBody.setText(posts.get(position).getBody());
        holder.tvUserId.setText(posts.get(position).getUserId().toString());
    }

    @Override
    public int getItemCount() {
        return posts.size();
    }

    public void updateItems(List items) {
        this.posts = items;
        notifyDataSetChanged();
    }

    private PostItem getItem(int pos) {
        return posts.get(pos);
    }

    public interface ItemListener {
        void onPostClick(long id);
    }    
}

8. Let’s make the first request from our MainActivity. If we want to consume the API asynchronously, we call the service as follows. Open the MainActivity.java and do the below changes.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;

import java.util.List;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

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

        final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rcView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        ApiInterface apiService = ApiClient.getClient().create(ApiInterface.class);

        Call<List<PostItem>> call = apiService.getPosts();

        call.enqueue(new Callback<List<PostItem>>() {
            @Override
            public void onResponse(Call<List<PostItem>>call, Response<List<PostItem>> response) {
                List<PostItem> posts = response.body();
                recyclerView.setAdapter(new PostAdapter(posts, R.layout.post_item, getApplicationContext(), 
                    new PostAdapter.ItemListener() {
                        @Override
                        public void onPostClick(long id) {
                            Toast.makeText(MainActivity.this, "Post ID is " + id, Toast.LENGTH_SHORT).show();
                        }
                    }));
            }

            @Override
            public void onFailure(Call<List<PostItem>>call, Throwable t) {
                Log.e(TAG, t.toString());
            }
        });
    }
}

enqueue() asynchronously sends the request and notifies your app with a callback when a response comes back. Since this request is asynchronous, Retrofit handles it on a background thread so that the main UI thread isn't blocked or interfered with.

To use enqueue(), you have to implement two callback methods:

  • onResponse() invoked for a received HTTP response. This method is called for a response that can be correctly handled even if the server returns an error message. So if you get a status code of 404 or 500, this method will still be called. To get the status code in order for you to handle situations based on them, you can use the method response.code(). You can also use the isSuccessful() method to find out if the status code is in the range 200-300, indicating success.
  • onFailure() invoked when a network exception occurred communicating to the server or when an unexpected exception occurred handling the request or processing the response.

So, Retrofit will download and parse the API data on a background thread, and then return the results back to the UI thread via the onResponse or onFailure method.

Result

android_retrofit_demo.png

Code on github

Useful links

comments powered by Disqus