Photo by Shubham Dhage on Unsplash
Implementing Retrofit (Part 02) in our App with MVVM & DI in Android: (Day 06)
Table of contents
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
andstatus
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
This class is NOT annotated with
@entity
likeUserData
class, because we do not want to savemsg
andstatus
, it'd only be used later to update UI components.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 theArrayList<UserData>?
variable being annotated asuser
.
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!