Android Data, Networking & APIs

Networking with Retrofit

18 min Lesson 6 of 12

Networking with Retrofit

In the previous lesson you made HTTP requests by manually opening an HttpURLConnection, reading an InputStream, and building the response yourself. That approach works, but it produces a lot of plumbing code that is easy to get wrong. Retrofit, from Square, turns your REST API into a Java interface. You describe what the endpoint looks like, and Retrofit handles the HTTP machinery for you — connection management, request serialization, response parsing, threading, and error handling.

Retrofit is the de-facto standard for REST calls in Android. Every professional Android codebase you will encounter uses it or something built on top of it. Understanding it deeply pays off immediately.

Adding Retrofit to Your Project

Open your module-level build.gradle and add these dependencies:

dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' }

retrofit is the core library. converter-gson plugs in Gson so Retrofit can automatically convert JSON to your Java model classes. The logging-interceptor is not required but is invaluable during development — it logs every request and response to Logcat.

You also need the internet permission in AndroidManifest.xml:

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

Step 1 — Define Your Model Classes

Retrofit with Gson maps JSON fields to Java fields by name. Define a plain Java class (a POJO) whose fields match the JSON keys you expect. For example, if the API returns user objects:

// models/User.java public class User { private int id; private String name; private String email; // Gson uses reflection; getters are optional but good practice public int getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } }
Field naming: If the JSON key uses snake_case (e.g. first_name) but you want a camelCase Java field (firstName), annotate with @SerializedName("first_name"). Alternatively, configure Gson with setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) when you build the GsonConverterFactory.

Step 2 — Declare the API Interface

Create a Java interface. Each method represents one endpoint. Retrofit annotations describe the HTTP method, URL path, query parameters, request body, and headers.

import java.util.List; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; public interface UserApiService { // GET /users — returns a list of users @GET("users") Call<List<User>> getUsers(); // GET /users/{id} — path parameter @GET("users/{id}") Call<User> getUser(@Path("id") int userId); // GET /users?page=2 — query parameter @GET("users") Call<List<User>> getUsersPage(@Query("page") int page); // POST /users — request body serialised to JSON @POST("users") Call<User> createUser(@Body User newUser); // PUT /users/{id} @PUT("users/{id}") Call<User> updateUser(@Path("id") int userId, @Body User updated); // DELETE /users/{id} @DELETE("users/{id}") Call<Void> deleteUser(@Path("id") int userId); }

The return type is always Call<T> where T is the type Retrofit should deserialize the response body into. Use Call<Void> when the response has no body (e.g. a 204 No Content).

Step 3 — Build the Retrofit Instance

The Retrofit object is expensive to create. Build it once and reuse it via a singleton — usually in a dedicated ApiClient class:

import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class ApiClient { private static final String BASE_URL = "https://jsonplaceholder.typicode.com/"; private static Retrofit retrofit = null; public static Retrofit getClient() { if (retrofit == null) { HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(logging) .build(); retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } public static UserApiService getUserService() { return getClient().create(UserApiService.class); } }
Base URL must end with a trailing slash. If you write "https://api.example.com/v1" (no slash) and your interface has @GET("users"), Retrofit resolves the URL incorrectly. Always end the base URL with /.

Step 4 — Making Asynchronous Calls

A Call object can be executed synchronously (call.execute(), but this blocks and must never run on the main thread) or asynchronously (call.enqueue(), which posts the result back on the main thread). Always use enqueue from an Activity or Fragment:

import android.os.Bundle; import android.util.Log; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class MainActivity extends AppCompatActivity { private static final String TAG = "RetrofitDemo"; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.textView); loadUsers(); } private void loadUsers() { Call<List<User>> call = ApiClient.getUserService().getUsers(); call.enqueue(new Callback<List<User>>() { @Override public void onResponse(Call<List<User>> call, Response<List<User>> response) { if (response.isSuccessful() && response.body() != null) { List<User> users = response.body(); StringBuilder sb = new StringBuilder(); for (User u : users) { sb.append(u.getName()).append("\n"); } textView.setText(sb.toString()); } else { // HTTP error (4xx / 5xx) Log.e(TAG, "HTTP error: " + response.code()); } } @Override public void onFailure(Call<List<User>> call, Throwable t) { // Network failure, timeout, or malformed response Log.e(TAG, "Network failure: " + t.getMessage()); } }); } }

Two callbacks are provided. onResponse fires whenever the server returned any HTTP response — including 404 or 500. Always check response.isSuccessful() (codes 200–299) before trusting response.body(). onFailure fires only for network-level failures: no internet, DNS failure, connection timeout, or a JSON parse error.

Posting Data with @Body

To create a resource, annotate a parameter with @Body. Retrofit serializes the Java object to JSON automatically:

private void createUser() { User newUser = new User(); // Gson reads the field values directly via reflection // so there is no need for a separate setter Call<User> call = ApiClient.getUserService().createUser(newUser); call.enqueue(new Callback<User>() { @Override public void onResponse(Call<User> call, Response<User> response) { if (response.isSuccessful()) { User created = response.body(); Log.d(TAG, "Created user with id: " + created.getId()); } } @Override public void onFailure(Call<User> call, Throwable t) { Log.e(TAG, "Failed: " + t.getMessage()); } }); }

Adding Authentication Headers

Most real APIs require an authorization header. The cleanest approach is an OkHttp interceptor that attaches the header to every request automatically:

OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(chain -> { okhttp3.Request original = chain.request(); okhttp3.Request authenticated = original.newBuilder() .header("Authorization", "Bearer " + getAuthToken()) .build(); return chain.proceed(authenticated); }) .build();

This way no individual API call needs to know about the token — a clean separation of concerns.

Do not cancel calls after the Activity is destroyed. If onResponse or onFailure fires after the Activity is gone it will try to update destroyed views and crash with a NullPointerException or IllegalStateException. Cancel any in-flight calls in onDestroy by keeping a reference: call.cancel(). In practice, ViewModels with LiveData (or coroutines in Kotlin) eliminate this problem by scoping network calls to the ViewModel lifecycle. For this tutorial, cancel in onDestroy.

Summary

Retrofit reduces REST networking to four steps: add the dependency, define model POJOs, declare an interface with annotations, and build a singleton Retrofit instance. Calls are made with enqueue, which delivers results on the main thread via onResponse and onFailure. In the next lesson you will learn how to parse more complex nested JSON structures by hand and with Gson.