Handling Two Different Responses in Retrofit
Retrofit, a popular REST client for Android, makes it easy to make network calls and parse responses. However, handling different types of responses within a single API endpoint can be tricky. This article explores two common scenarios where you might need to handle two different responses and provides solutions.
Scenario 1: Different Response Data Structures
Problem
Your API endpoint returns different data structures depending on certain conditions. For example, it might return a list of users if the user is authenticated and an error message if not.
Solution: Using Type Adapters
Retrofit’s Type Adapters allow you to define custom logic for converting JSON responses into Java objects. You can use this mechanism to handle different response structures gracefully.
Implementation
- Create a Sealed Class: Define a sealed class to represent the different response types. This helps ensure type safety.
- Define Type Adapters: Create Type Adapters for each subclass of the sealed class, responsible for parsing the corresponding JSON structure.
- Configure Retrofit: Register the Type Adapters in your Retrofit builder.
Example
// Sealed class for different response types sealed class UserResponse { data class Success(val users: List<User>) : UserResponse() data class Error(val message: String) : UserResponse() } // Type adapter for Success response class SuccessTypeAdapter : JsonDeserializer<UserResponse.Success> { override fun deserialize( json: JsonElement, type: Type?, context: JsonDeserializationContext? ): UserResponse.Success { val users = Gson().fromJson(json, Array<User>::class.java).toList() return UserResponse.Success(users) } } // Type adapter for Error response class ErrorTypeAdapter : JsonDeserializer<UserResponse.Error> { override fun deserialize( json: JsonElement, type: Type?, context: JsonDeserializationContext? ): UserResponse.Error { val message = json.asJsonObject.get("message").asString return UserResponse.Error(message) } } // Retrofit configuration Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create().registerTypeAdapter( UserResponse::class.java, object : JsonDeserializer<UserResponse> { override fun deserialize( json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext? ): UserResponse { val successTypeAdapter = SuccessTypeAdapter() val errorTypeAdapter = ErrorTypeAdapter() return try { successTypeAdapter.deserialize(json, typeOfT, context) } catch (e: Exception) { errorTypeAdapter.deserialize(json, typeOfT, context) } } } )) .build()
Output
// Success response { "users": [ { "id": 1, "name": "John Doe" }, { "id": 2, "name": "Jane Doe" } ] } // Error response { "message": "Authentication required" }
Scenario 2: Different HTTP Status Codes
Problem
Your API endpoint returns different status codes depending on the outcome of the request. For example, it might return a 200 OK for success and a 400 Bad Request for validation errors.
Solution: Using Call Adapters
Call Adapters allow you to intercept Retrofit calls and handle responses based on the HTTP status code.
Implementation
- Define a Custom Call Adapter: Create a custom Call Adapter that maps status codes to different response types.
- Register the Adapter: Register your custom adapter in your Retrofit builder.
- Handle Responses: Define separate methods to handle different response types.
Example
// Custom call adapter class CustomCallAdapter : CallAdapter<Response<Any>, UserResponse> { override fun adapt(call: Call<Response<Any>>): UserResponse { val response = call.execute() return when (response.code()) { 200 -> { val userResponse = Gson().fromJson( response.body().toString(), UserResponse::class.java ) UserResponse.Success(userResponse.users) } 400 -> { val errorResponse = Gson().fromJson( response.errorBody()?.string(), ErrorResponse::class.java ) UserResponse.Error(errorResponse.message) } else -> { // Handle other status codes as needed UserResponse.Error("Unknown error") } } } override fun responseType(): Type { return UserResponse::class.java } } // Retrofit configuration Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(CustomCallAdapter.Factory()) .build()
Output
// Success response (status code 200) // Same as the Success response in Scenario 1 // Error response (status code 400) { "message": "Invalid input" }
Comparison
Here is a comparison of the two approaches:
Feature | Type Adapters | Call Adapters |
---|---|---|
Purpose | Handle different response data structures | Handle different HTTP status codes |
Flexibility | Provides fine-grained control over JSON parsing | Suitable for handling different response types based on status codes |
Complexity | Can be more complex to implement | Generally simpler to use |
Conclusion
Retrofit provides powerful tools to handle various response scenarios. Understanding and choosing the appropriate approach between Type Adapters and Call Adapters is crucial for building robust and maintainable network interactions in your Android app.