0

I'm struggling to maintain consistent Types in my Kotlin application across Retrofit, Room, and RxJava2 due to the JSON key naming convention that an API uses. I've attempted to research the best way to tackle the problem below, but I can't think of a better way to solve it, and I'm interested in what the community has to offer.

Consider the following example;

I'm using the Kotlin language on Android, and performing network requests with Retrofit and GSON. The API I'm using provides JSON with a different root key name depending on whether the response is an Array or a single Object.

The payload of each Object is the same, but if the response is an Array, the root key is plural.

For example, let's say I have a Data Class defined for a user:

data class User(var id: Int?, name: String?)

If I make an API request for a single user, the API returns the following:

{
  "user": {
    "id": 1,
    "name: "Sam"
  }
}

If I make an API request for multiple users, the API returns the following:

{
  "users": [{
    "id": 1,
    "name: "Sam"
  }]
}

The payload is the same, but the root key changes between user and users. I've created a Data Class to handle the separate root keys:

data class Users(
        @SerializedName("users") var users: List<User>?,
        @SerializedName("user") var user: User?
)

And when making the separate API requests, I use the Users Data Class for both:

/** GET single user by ID */
@GET("users/{id}")
    fun getUser(
        @Path("id") id: Int?): Single<Users>

/** GET list of users */
@GET("users")
    fun getUsers(): Single<Users>

And when I save the response to the Room database, I access it using response.users or response.user respectively. Room is set up to return Queries as either User or List<User>, and this creates a problem when using the Retrofit requests in an RxJava2 stream. For example, this is a very simple stream if the Room database returns Maybe<List<User>>:

usersRepository.getAll()
    .switchIfEmpty(api.getUsers())

The result of usersRepository.getAll() is List<User>, but if the result from the database is empty and I attempt to fallback on an API request instead, RxJava2 shows an Incompatible Type error because the api.getUsers() request returns the Type Single<Users> rather than Single<List<User>>. If the API request was able to use the same type, I wouldn't have a problem, but I'm not able to specify Single<List<User>> on the Retrofit request because GSON can't deserialise the response correctly.

Because of this, from my current understanding of the issue (which could be layman and I'm open to being moulded), I'm separating the database and API requests, and doing a lot of empty/null checking, which prevents me from keeping everything in one nice and tidy RxJava2 stream.

I hope this question is clear enough, and I value anyone's input. Thank you for your time!

Sam Caplat
  • 50
  • 7

1 Answers1

1

This is a very common problem and what people like to do is to create a wrapper class that deals with conversions, validations, and all other painful jobs, and use that class to do network requests instead of calling the retrofit api directly.

For example, consider creating a class like this:

class UserClient {

    private val userApi: UserApi

    fun getUser(id: Int): Single<User> {
        return userApi.getUser(id) // This has a type of Single<Users>
            .map { it.user } // now it's map to Single<User>
    }

    fun getUsers(): Single<List<User>> {
        return userApi.getUsers() // This has a type of Single<Users>
            .doOnSuccess { Log.d(TAG, "yay!")}
            .doOnError { e -> Log.d(TAG, "Something went wrong: $e")}
            .map { it.users } // Now it's map to Single<List<User>>
    }
}

Then you can do something like this:

usersRepository.getAll()
    .switchIfEmpty(UserClient.getInstance().getUsers())
Sanlok Lee
  • 3,404
  • 2
  • 15
  • 25