Retrofit: Type-Safe HTTP Client for Java and Android

Retrofit is a popular HTTP client library for Java and Android that turns your HTTP API into a Java interface. It simplifies HTTP calls by handling serialization, deserialization, and execution, making API integration clean and type-safe.


Why Retrofit?

Advantages over traditional HTTP clients:

  • Type-safe - Compile-time checking of API contracts
  • Annotation-based - Declarative API definition
  • Automatic serialization - JSON, XML, Protocol Buffers, etc.
  • Synchronous & Asynchronous support
  • Pluggable - Custom converters and adapters
  • Clean code - Separation of concerns

Core Concepts

  • Interface - Define your API endpoints as Java interface methods
  • Annotations - Specify HTTP methods, headers, parameters
  • Converter - Handle request/response serialization (Gson, Jackson, etc.)
  • Call Adapter - Convert responses to different types (RxJava, Coroutines, etc.)

Getting Started

Dependencies (Maven)

<dependencies>
<!-- Retrofit Core -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.9.0</version>
</dependency>
<!-- GSON Converter -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.9.0</version>
</dependency>
<!-- Optional: RxJava Adapter -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>adapter-rxjava3</artifactId>
<version>2.9.0</version>
</dependency>
<!-- Optional: Logging Interceptor -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>4.10.0</version>
</dependency>
</dependencies>

Basic Usage Examples

Example 1: Basic Setup and GET Request

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import retrofit2.http.Path;
import java.util.List;
public class RetrofitBasicDemo {
// Define API interface
public interface GitHubService {
@GET("users/{username}/repos")
Call<List<Repository>> listRepos(@Path("username") String username);
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);
}
// Data models
static class Repository {
private String name;
private String description;
private boolean fork;
private int stargazers_count;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public boolean isFork() { return fork; }
public void setFork(boolean fork) { this.fork = fork; }
public int getStargazersCount() { return stargazers_count; }
public void setStargazersCount(int stargazers_count) { this.stargazers_count = stargazers_count; }
@Override
public String toString() {
return String.format("Repository{name='%s', description='%s', stars=%d}", 
name, description, stargazers_count);
}
}
static class User {
private String login;
private String name;
private int public_repos;
private int followers;
// Getters and setters
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getPublicRepos() { return public_repos; }
public void setPublicRepos(int public_repos) { this.public_repos = public_repos; }
public int getFollowers() { return followers; }
public void setFollowers(int followers) { this.followers = followers; }
@Override
public String toString() {
return String.format("User{login='%s', name='%s', repos=%d, followers=%d}", 
login, name, public_repos, followers);
}
}
public static void main(String[] args) {
// Create Retrofit instance
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
// Create service instance
GitHubService service = retrofit.create(GitHubService.class);
// Synchronous call
System.out.println("=== Synchronous Call ===");
try {
Call<User> call = service.getUser("octocat");
Response<User> response = call.execute();
if (response.isSuccessful()) {
User user = response.body();
System.out.println("User: " + user);
} else {
System.out.println("Error: " + response.code() + " - " + response.message());
}
} catch (Exception e) {
e.printStackTrace();
}
// Asynchronous call
System.out.println("\n=== Asynchronous Call ===");
Call<List<Repository>> reposCall = service.listRepos("octocat");
reposCall.enqueue(new Callback<List<Repository>>() {
@Override
public void onResponse(Call<List<Repository>> call, Response<List<Repository>> response) {
if (response.isSuccessful()) {
List<Repository> repos = response.body();
System.out.println("Found " + repos.size() + " repositories:");
repos.forEach(repo -> System.out.println(" - " + repo));
} else {
System.out.println("Error: " + response.code() + " - " + response.message());
}
}
@Override
public void onFailure(Call<List<Repository>> call, Throwable t) {
System.err.println("Request failed: " + t.getMessage());
}
});
// Wait for async call to complete
try { Thread.sleep(5000); } catch (InterruptedException e) {}
}
}

Example 2: Comprehensive API Service with Multiple HTTP Methods

import retrofit2.Call;
import retrofit2.http.*;
import java.util.List;
import java.util.Map;
public interface UserApiService {
// GET with query parameters
@GET("users")
Call<List<User>> getUsers(
@Query("page") int page,
@Query("limit") int limit,
@Query("sort") String sortBy
);
// GET with path parameter
@GET("users/{id}")
Call<User> getUserById(@Path("id") Long userId);
// POST with body
@POST("users")
Call<User> createUser(@Body User user);
// PUT with path parameter and body
@PUT("users/{id}")
Call<User> updateUser(@Path("id") Long userId, @Body User user);
// PATCH with specific fields
@PATCH("users/{id}")
Call<User> patchUser(@Path("id") Long userId, @Body Map<String, Object> updates);
// DELETE
@DELETE("users/{id}")
Call<Void> deleteUser(@Path("id") Long userId);
// GET with dynamic query parameters
@GET("users/search")
Call<List<User>> searchUsers(@QueryMap Map<String, String> filters);
// POST with form data
@FormUrlEncoded
@POST("users/login")
Call<AuthResponse> login(
@Field("username") String username,
@Field("password") String password
);
// GET with headers
@GET("users/profile")
Call<UserProfile> getProfile(@Header("Authorization") String authToken);
// Multiple headers
@GET("users/secure")
Call<User> getSecureUser(
@Header("Authorization") String token,
@Header("X-API-Key") String apiKey
);
}
// Data models
class User {
private Long id;
private String name;
private String email;
private String role;
// Constructors, getters, setters
public User() {}
public User(String name, String email, String role) {
this.name = name;
this.email = email;
this.role = role;
}
// Getters and setters...
}
class AuthResponse {
private String token;
private User user;
// Getters and setters...
}
class UserProfile {
private User user;
private Map<String, Object> preferences;
// Getters and setters...
}

Advanced Features

Example 3: Custom Interceptor and Logging

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class AdvancedRetrofitSetup {
public static Retrofit createRetrofitClient() {
// Create logging interceptor
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
// Create custom interceptor for headers
okhttp3.Interceptor authInterceptor = chain -> {
okhttp3.Request original = chain.request();
// Add custom headers
okhttp3.Request request = original.newBuilder()
.header("User-Agent", "MyApp/1.0")
.header("Accept", "application/json")
.header("Cache-Control", "no-cache")
.method(original.method(), original.body())
.build();
return chain.proceed(request);
};
// Create OkHttp client with interceptors
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(logging)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
// Build Retrofit with custom client
return new Retrofit.Builder()
.baseUrl("https://api.example.com/v1/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
// Usage
public static void main(String[] args) {
Retrofit retrofit = createRetrofitClient();
UserApiService service = retrofit.create(UserApiService.class);
// Now all requests will have custom headers and logging
}
}

Example 4: Error Handling and Custom Response

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class ErrorHandlingExample {
public static <T> void executeWithErrorHandling(Call<T> call, ApiCallback<T> callback) {
call.enqueue(new Callback<T>() {
@Override
public void onResponse(Call<T> call, Response<T> response) {
if (response.isSuccessful()) {
callback.onSuccess(response.body());
} else {
try {
String errorBody = response.errorBody() != null ? 
response.errorBody().string() : "Unknown error";
callback.onError(new ApiException(
response.code(), 
"HTTP " + response.code() + ": " + errorBody
));
} catch (Exception e) {
callback.onError(new ApiException(
response.code(), 
"HTTP " + response.code() + ": " + response.message()
));
}
}
}
@Override
public void onFailure(Call<T> call, Throwable t) {
callback.onError(new ApiException(0, "Network error: " + t.getMessage()));
}
});
}
// Custom callback interface
public interface ApiCallback<T> {
void onSuccess(T response);
void onError(ApiException error);
}
// Custom exception
public static class ApiException extends Exception {
private final int statusCode;
public ApiException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
public int getStatusCode() { return statusCode; }
}
// Usage example
public static void main(String[] args) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
executeWithErrorHandling(service.getUser("nonexistentuser"), new ApiCallback<User>() {
@Override
public void onSuccess(User response) {
System.out.println("Success: " + response);
}
@Override
public void onError(ApiException error) {
System.err.println("API Error (" + error.getStatusCode() + "): " + error.getMessage());
}
});
}
}

Reactive Programming with RxJava

Example 5: RxJava Integration

import retrofit2.Retrofit;
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import retrofit2.http.Path;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class RxJavaRetrofitExample {
public interface RxGitHubService {
@GET("users/{username}")
Single<User> getUserRx(@Path("username") String username);
@GET("users/{username}/repos")
Observable<Repository> getReposRx(@Path("username") String username);
}
public static void main(String[] args) throws InterruptedException {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build();
RxGitHubService service = retrofit.create(RxGitHubService.class);
System.out.println("=== Single Observable (One item) ===");
service.getUserRx("octocat")
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.map(user -> {
// Transform data
return String.format("User: %s (%d followers)", user.getName(), user.getFollowers());
})
.subscribe(
result -> System.out.println("Success: " + result),
error -> System.err.println("Error: " + error.getMessage())
);
System.out.println("\n=== Observable Stream (Multiple items) ===");
service.getReposRx("octocat")
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.filter(repo -> !repo.isFork()) // Only non-forked repos
.take(5) // Limit to first 5
.map(repo -> repo.getName() + " - " + repo.getStargazersCount() + " stars")
.subscribe(
repo -> System.out.println("Repo: " + repo),
error -> System.err.println("Error: " + error.getMessage()),
() -> System.out.println("Completed processing repositories")
);
// Wait for async operations
Thread.sleep(5000);
}
}

Custom Converters and Serialization

Example 6: Custom GSON Configuration

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CustomGsonExample {
public static Retrofit createCustomGsonRetrofit() {
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
.setPrettyPrinting()
.serializeNulls() // Include null values
.create();
return new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
// Custom adapter for LocalDateTime
static class LocalDateTimeAdapter implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
private final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
@Override
public JsonElement serialize(LocalDateTime src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(formatter.format(src));
}
@Override
public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
return LocalDateTime.parse(json.getAsString(), formatter);
}
}
}

Testing with MockWebServer

Example 7: Unit Testing with MockWebServer

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.io.IOException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class GitHubServiceTest {
private MockWebServer mockWebServer;
private GitHubService service;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.build();
service = retrofit.create(GitHubService.class);
}
@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
void testGetUserSuccess() throws IOException {
// Arrange
String jsonResponse = """
{
"login": "testuser",
"name": "Test User",
"public_repos": 42,
"followers": 100
}
""";
mockWebServer.enqueue(new MockResponse()
.setBody(jsonResponse)
.setResponseCode(200)
.addHeader("Content-Type", "application/json"));
// Act
Call<User> call = service.getUser("testuser");
Response<User> response = call.execute();
// Assert
assertTrue(response.isSuccessful());
User user = response.body();
assertNotNull(user);
assertEquals("testuser", user.getLogin());
assertEquals("Test User", user.getName());
assertEquals(42, user.getPublicRepos());
}
@Test
void testGetUserNotFound() {
// Arrange
mockWebServer.enqueue(new MockResponse()
.setResponseCode(404)
.setBody("{\"message\": \"Not Found\"}"));
// Act
Call<User> call = service.getUser("nonexistent");
// Assert
try {
Response<User> response = call.execute();
assertFalse(response.isSuccessful());
assertEquals(404, response.code());
} catch (IOException e) {
fail("Request failed", e);
}
}
}

Best Practices

  1. Base URL Management
public class ApiClient {
private static final String BASE_URL = "https://api.example.com/v1/";
private static Retrofit retrofit;
public static synchronized Retrofit getClient() {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
}
  1. Singleton Pattern
public class ApiServiceProvider {
private static UserApiService instance;
public static synchronized UserApiService getService() {
if (instance == null) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
instance = retrofit.create(UserApiService.class);
}
return instance;
}
}
  1. Error Handling Strategy
public class ApiResponse<T> {
private final T data;
private final String error;
private final int statusCode;
// Factory methods for success/error
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(data, null, 200);
}
public static <T> ApiResponse<T> error(String error, int statusCode) {
return new ApiResponse<>(null, error, statusCode);
}
}

Conclusion

Retrofit provides a clean, type-safe way to consume HTTP APIs in Java:

Key Features:

  • Declarative API - Define endpoints as Java interfaces
  • Type Safety - Compile-time checking of API contracts
  • Flexible Serialization - Support for JSON, XML, Protobuf, etc.
  • Synchronous & Asynchronous - Both blocking and non-blocking calls
  • Interceptors - For logging, authentication, and caching
  • Testing Support - Easy integration with MockWebServer

When to Use Retrofit:

  • REST API consumption in Java/Android applications
  • Type-safe API clients
  • Applications requiring clean separation of network logic
  • Projects using reactive programming (RxJava, Coroutines)

Performance Benefits:

  • Connection pooling and reuse
  • Efficient serialization/deserialization
  • Minimal boilerplate code
  • Optimized for mobile and server applications

Retrofit has become the de facto standard for HTTP communication in the Java ecosystem, particularly in Android development, due to its simplicity, power, and extensive community support.


Production Tips: Always use interceptors for logging in development, implement proper error handling, consider using Hystrix or Resilience4j for circuit breaking, and cache responses where appropriate to improve performance and reduce network calls.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper