Handling Two Different Responses in Retrofit

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

  1. Create a Sealed Class: Define a sealed class to represent the different response types. This helps ensure type safety.
  2. Define Type Adapters: Create Type Adapters for each subclass of the sealed class, responsible for parsing the corresponding JSON structure.
  3. 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

  1. Define a Custom Call Adapter: Create a custom Call Adapter that maps status codes to different response types.
  2. Register the Adapter: Register your custom adapter in your Retrofit builder.
  3. 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.


Leave a Reply

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