Implementing Retrofit (Part 02) in our App with MVVM & DI in Android: (Day 06)

In our previous blog post, we understood why we use Retrofit and how should it be implemented in an MVVM/DI-based architecture. But before we move to our next component, we need to look into a few more concepts/features that Retrofit provides.

And to understand these concepts, let's go back to our previous post and look at the data, which we received in response inside the MainActivity class.

E Success: {"msg":"Successfully fecthed all users","user":[{"_id":"650950212774b400337c3d98","first_name":"Test","last_name":"Name","password":"Test@1234","phone_number":"123456789","email_id":"test@gmail.com","__v":0}],"status":"success"}

Let's structure the JSON response into a separate file for better readability.

{
  "msg": "Successfully fecthed all users",
  "user": [
    {
      "_id": "650950212774b400337c3d98",
      "first_name": "Test",
      "last_name": "Name",
      "password": "Test@1234",
      "phone_number": "123456789",
      "email_id": "test@gmail.com",
      "__v": 0
    }
  ],
  "status": "success"
}

The Problem

This is a relatively simple JSON response, but in complex projects, such responses could be extremely convoluted or nested, which means parsing them and getting valuable data out of them could be a challenge.

Solution

The code to this blog post can be found here.

An elegant solution for both sending and receiving JSON data as well as parsing them is provided inherently by Retrofit i.e. @SerializedName

@SerializedName is an annotation used to specify the name of the field as it appears in the JSON response or request body. This annotation is part of the Gson converter, which is a library used to convert Java Objects into their JSON representation and vice versa.

Let's try to update our model class to implement this @SerializedName to figure out how it could be helpful.

@Entity(tableName = "UserData")
class UserData {
    @PrimaryKey(autoGenerate = false)
    @SerializedName("_id")
    var uid: String = ""

    @SerializedName("first_name")
    var firstName: String? = null

    @SerializedName("last_name")
    var lastName: String? = null

    @SerializedName("phone_number")
    var phoneNumber: String? = null

    @SerializedName("email_id")
    var emailId: String? = null

    @SerializedName("password")
    var password: String? = null
}

Notice, how each file is annotated with its corresponding JSON element name.

But, this is still not enough.

If you notice the JSON response which we received from our API call you'd notice the following additional data as well.

  • The JSON data has two fields i.e. msg and status which we might need to parse errors or special messages.

  • The user data received is in the form of a list, i.e. this API returns not just one user data but the list of all available users.

But if you notice our model class, you'd know that it can only parse a simple UserData JSON object.

To solve this issue, we can simply create another JSON Parser class that will have the necessary fields like msg and status, and in that class, we can also add our UserData JSON parser class as a reference.

If the above description is not clear enough, look at the code below.

class UserDataResponse {
    @SerializedName("msg")
    var msg: String? = null

    @SerializedName("user")
    var data: ArrayList<UserData>? = null //This will help in parsing the list of UserData JSON objects

    @SerializedName("status")
    var status: String? = null
}

There are two important things to notice here

  1. This class is NOT annotated with @entity like UserData class, because we do not want to save msg and status, it'd only be used later to update UI components.

  2. If you refer to the JSON response from above, you'll notice that the list of users was present inside an element named user, this is what we are trying to achieve with the ArrayList<UserData>? variable being annotated as user.

Now that we have created our custom new JSON parser, we also need to use it to parse data.

To do this we need to make the following changes to our code

Step 1

interface WebService {
    @GET("users")
    fun fetchAllUsers(): Call<UserDataResponse> //Change the response type to UserDataResponse
}

First, we need to change the response type to Call<UserDataResponse>

Step 2

Now, let's go ahead and update the response type mentioned in our override methods for getting the response in MainActivity.

insertButton.setOnClickListener {
            webService.fetchAllUsers().enqueue(object : Callback<UserDataResponse> {
                override fun onResponse(call: Call<UserDataResponse>, response: Response<UserDataResponse>) {
                    if (response.isSuccessful) {
                        // Log the successful response
                        val responseObj = response.body()
                        Log.e("API_RESPONSE", "Success: ${Gson().toJson(responseObj)}")
                    } else {
                        // Log the unsuccessful response
                        val errorString = response.errorBody()?.string()
                        Log.e("API_RESPONSE", "Unsuccessful: $errorString")
                    }
                }

                override fun onFailure(call: Call<UserDataResponse>, t: Throwable) {
                    // Log the failure message
                    Log.e("API_RESPONSE", "Failure: ${t.message}")
                }
            })
        }

And that's it!

Now parsing through the JSON string should be as simple as iterating through a simple Java object.

Notice here that now, response.body() is essentially a UserDataResponse object, under which you can find the response data points like msg, status, and the list of users as well!

Result

This is still a fairly basic implementation of Retrofit, but it does give us an insight into how using this library can make development process efficient by removing the need to parse and create JSON requests every time we want to make a network call, and at the same time by making the code much cleaner to read and understand.

In our next blog post, we'll explore the next layer of our MVVM architecture i.e. the repository class. We'll try to understand why is it needed in the first place and then how can we actually implement it. Till then, happy coding!