Implementing Retrofit (Part 01) in our App with MVVM & DI in Android: (Day 05)
In our last blog post, we set up the RoomDB in our project so that we could save data to our local database. We also tried inserting some data into one of our tables. In today's section of this series, we'll try to inject a Retrofit client into our project so that we can send and receive this data over a remote server as well.
Before We Start
The code to this blog post can be found here.
Since we'll need an API service to which we can send and receive data, I have already created and hosted a Node Server here. If you'd like to create your own version, you can use any online free service like Render.
We have the following APIs available for creating and validating user
URL | Query Type | Used For |
https://login-flow-27d5.onrender.com/api/users | GET | Fetch all available user data |
https://login-flow-27d5.onrender.com/api/users | POST | Create a new user (body of the request discussed later in the post) |
https://login-flow-27d5.onrender.com/api/login | GET | Validate login request |
What is Retrofit & OkHTTP2 ?
Before we start the implementation, here's an overview of what exactly are these two libraries.
OkHttp
and Retrofit
are both open-source tools used in Android and Java applications to handle network operations, but they serve slightly different purposes and can often be used together to complement each other. Here’s how they differ:
OkHttp
Low-Level Networking Library: OkHttp is a low-level HTTP client used for sending and receiving HTTP requests and responses. It is more about handling the HTTP protocol, including connection pooling, caching, and handling requests and responses.
Request Customization: OkHttp allows for detailed customization of requests, including setting timeouts, adding headers, and more.
Interceptors: OkHttp supports interceptors, which can be used to monitor, rewrite, and retry calls.
WebSocket Support: OkHttp supports WebSocket communication, which facilitates real-time data exchange in a more efficient manner compared to HTTP polling.
Manual JSON Parsing: If you are using OkHttp alone, you would have to manually parse JSON responses using a library like Gson or Moshi.
Retrofit
High-Level REST Client: Retrofit is a type-safe HTTP client for Android and Java. It is built on top of OkHttp and leverages OkHttp’s features. It is more about making it easier to connect to RESTful web services and APIs.
Annotation-Based API Definitions: Retrofit allows developers to define APIs through annotations, which makes the code cleaner and easier to maintain.
Automatic JSON Parsing: Retrofit can automatically parse JSON responses into Java objects using converters like Gson or Moshi, saving developers time and reducing boilerplate code.
Synchronous and Asynchronous Calls: Retrofit supports both synchronous and asynchronous network calls, allowing developers to choose the best approach for their needs.
Integration with RxJava: Retrofit can be integrated with RxJava, facilitating reactive programming and making it easier to handle asynchronous tasks and event-based programs.
Working Together
Complementary: OkHttp and Retrofit can work together, with Retrofit leveraging OkHttp for HTTP requests while providing a high-level, user-friendly interface for API interactions.
Efficiency and Performance: Using Retrofit with OkHttp combines the efficiency and performance of OkHttp with the ease of use of Retrofit, resulting in a powerful toolset for network operations in Android and Java applications.
Best of Both Worlds: Developers get the best of both worlds: the low-level control of OkHttp and the high-level functionalities of Retrofit.
Now that we have an overall understanding of what these two libraries do, let's start the implementation.
Step 1
Let's start by implementing the required libraries in our app.gradle
file
dependencies {
//Other Libraries
def dagger_version = '2.46.1'
implementation "com.google.dagger:dagger:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"
//dagger android
implementation "com.google.dagger:dagger-android:$dagger_version"
implementation "com.google.dagger:dagger-android-support:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
//room db
// Room components
implementation "androidx.room:room-runtime:2.5.2"
kapt "androidx.room:room-compiler:2.5.2"
implementation "androidx.room:room-ktx:2.5.2"
//Retrofit Implementation
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3' }
Step 2
Since the need for retrofit and database is mostly for similar use cases, we can put them in the same module for simpler projects like ours. So let's go ahead and provide OkHTTP client to our Dagger graph first.
@Module
class DatabaseModule {
@Provides
@Singleton
fun provideDatabase(application: Application): AppDatabase {
return Room.databaseBuilder(application,
AppDatabase::class.java, "appDatabase.db")
.fallbackToDestructiveMigration()
.build()
}
@Provides
@Singleton
fun provideUserDAO(database: AppDatabase) = database.userDAO()
//OkHttpClient provided to the dagger graph
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
val request = chain.request()
chain.proceed(request)
}).build()
}
}
Explanation
.addInterceptor(Interceptor { chain -> ... })
: Here, we are adding an interceptor to theOkHttpClient
. Interceptors are used to monitor, rewrite, and retry calls. In this block, we define an interceptor using a lambda expression where we get the ongoingchain
.val request = chain.request()
: Inside the interceptor, we get the original request that was made.chain.proceed(request)
: We proceed with the original request. This means that we are not modifying the request in any way before sending it. If you wanted to modify the request (for example, to add a header to every request), this would be the place to do it..build()
: Finally, we call thebuild()
method to create theOkHttpClient
instance with the configurations we specified in the builder.
Step 3
Now that we have provided OkHttp client to our application, the next important component would be providing GSON
to our project, this would later be needed by the retrofit client.
@Provides
@Singleton
fun provideGson(): Gson {
return GsonBuilder()
.enableComplexMapKeySerialization()
.serializeNulls()
.setPrettyPrinting()
.setLenient()
.create()
}
Explanation
GsonBuilder initialization
GsonBuilder()
: Initiates the builder for creating a Gson instance with specific configurations.
GsonBuilder configurations
enableComplexMapKeySerialization()
: Allows Gson to handle complex map keys during the serialization process.serializeNulls()
: Instructs Gson to serialize null values, including them in the JSON output.setPrettyPrinting()
: Enables pretty printing, formatting the JSON output with line breaks and indentation for better readability.setLenient()
: Makes Gson lenient, allowing it to handle and parse invalid JSON without throwing exceptions.
Gson instance creation
create()
: Creates a Gson instance with the specified configurations set in the GsonBuilder.
Step 4
Now let's go ahead and provide the retrofit client as well to our dependency graph as follows.
@Provides
@Singleton
fun provideRetrofit(gson: Gson, okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl("https://login-flow-27d5.onrender.com/api/")
.client(okHttpClient)
.build()
}
Explanation
Retrofit Builder initialization
Retrofit.Builder()
: Initiates the builder for creating a Retrofit instance with specific configurations.
Retrofit Builder configurations
addConverterFactory(GsonConverterFactory.create(gson))
: Adds a converter factory to Retrofit for serializing and deserializing objects using the provided Gson instance.baseUrl("
https://login-flow-27d5.onrender.com/api/
")
: Sets the base URL for all Retrofit requests.client(okHttpClient)
: Sets the OkHttpClient to be used by Retrofit for making network requests.
Retrofit instance creation
build()
: Creates a Retrofit instance with the specified configurations set in the Retrofit Builder.
Step 5
Now that we have provided our retrofit client to the dagger graph, we can go ahead and create a new WebService
class, we'll use this class to organize all the network calls that we'll make in the future.
interface WebService {
@GET("users")
fun fetchAllUsers(): Call<ResponseBody> //This will be used to fetch all the users avialable
}
WebService as an Interface
Reason for being an interface: The
WebService
is defined as an interface because it serves as a contract that defines the endpoints of your API. It doesn't contain any implementation details; it only defines what methods are available and what kind of requests they should make.Interpretation by Retrofit: Retrofit uses this interface to generate an implementation at runtime using reflection. This generated implementation will take care of making the actual network requests and parsing the responses according to the details specified in the interface.
Annotations and HTTP Methods
- @GET annotation: The
@GET("users")
annotation tells Retrofit that this method should make a GET request to the "users" endpoint relative to the base URL specified in your Retrofit builder.
- @GET annotation: The
Return Type
- Call<ResponseBody>: The return type indicates that this method initiates a network call and returns an
Call
object that represents the HTTP request. TheResponseBody
type means that the raw response body can be accessed, allowing you to handle it as a string or some other format in your callback methods.
- Call<ResponseBody>: The return type indicates that this method initiates a network call and returns an
Step 6
Now that we have a WebService
available, we can inject it as well to our dagger graph as follows.
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): WebService {
return retrofit.create(WebService::class.java)
}
Explanation
Method Body
retrofit.create(WebService::
class.java
)
: This line of code uses thecreate
method of theRetrofit
class to create an implementation of theWebService
interface. TheWebService::
class.java
syntax is used to get aClass
object representing theWebService
interface, which is needed to tell Retrofit which interface to implement.
Working with Retrofit
- Retrofit's role: Retrofit uses the
WebService
interface to generate an implementation at runtime. This implementation will handle the HTTP requests defined in the interface, including setting up the HTTP method, headers, query parameters, and request body as needed, based on the annotations and parameters in the interface.
- Retrofit's role: Retrofit uses the
And that's all! Now we can go ahead and inject our retrofit client into MainActivity
.
Step 7
class MainActivity : DaggerAppCompatActivity() {
@Inject
lateinit var userDAO: UserDAO
@Inject
lateinit var webService: WebService //web service was injected here
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val insertButton = findViewById<Button>(R.id.insert)
insertButton.setOnClickListener {
webService.fetchAllUsers().enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
if (response.isSuccessful) {
// Log the successful response
val responseString = response.body()?.string()
Log.e("API_RESPONSE", "Success: $responseString")
} else {
// Log the unsuccessful response
val errorString = response.errorBody()?.string()
Log.e("API_RESPONSE", "Unsuccessful: $errorString")
}
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
// Log the failure message
Log.e("API_RESPONSE", "Failure: ${t.message}")
}
})
}
}
}
Explanation
Asynchronous Request
enqueue(object : Callback<ResponseBody> {...})
: This method is used to send the request asynchronously, meaning it will not block the main thread. It takes aCallback<ResponseBody>
object as a parameter, where you define how to handle the response and failure cases.
Callback Implementation
object : Callback<ResponseBody> {...}
: Here, an anonymous class implementing theCallback
interface is created to handle the response and failure of the network call.
Handling Response
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>)
: This method is called when a response is received. It takes two parameters: theCall
object representing the request and theResponse
object representing the response.if (response.isSuccessful)
: Checks if the response code is in the range 200-299, indicating a successful HTTP response.val responseString = response.body()?.string()
: If the response is successful, it retrieves the response body as a string and stores it in theresponseString
variable.Log.e("API_RESPONSE", "Success: $responseString")
: Logs the successful response using the error log level (henceLog.e
), although it's generally better to use a different log level such asLog.d
orLog.i
for successful responses.else: Handles the case where the response is not successful (i.e., the response code is outside the range 200-299).
val errorString = response.errorBody()?.string()
: If the response is not successful, it retrieves the error body as a string and stores it in theerrorString
variable.Log.e("API_RESPONSE", "Unsuccessful: $errorString")
: Logs the unsuccessful response, including the error body.
Handling Failure
override fun onFailure(call: Call<ResponseBody>, t: Throwable)
: This method is called when the network call fails due to a reason like a network error, timeout, etc. It takes two parameters: theCall
object representing the request and aThrowable
representing the error.Log.e("API_RESPONSE", "Failure: ${t.message}")
: Logs the failure message, which contains details about why the network call failed.
Result
Now, if you click on the Insert button, it should show you the data of all the available users as follows in the logcat.
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"}
Conclusion
Today's blog post just scratches the surface of Retrofit implementation, there's a lot more that we'll learn as we move ahead in our series and try to implement more complex scenarios.
An important thing to keep in mind is that the current version of implementation is only to help us understand the principles behind these libraries and implementation practices and is NOT how retrofit is implemented is production-ready apps, this implementation model will evolve significantly as we move forward in our series. Till then, Happy Coding!
Subscribe to my newsletter
Read articles from Abou Zuhayr directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Abou Zuhayr
Abou Zuhayr
Hey, I’m Abou Zuhayr, Android developer by day, wannabe rockstar by night. With 6 years of Android wizardry under my belt and currently working at Gather.ai, I've successfully convinced multiple devices to do my bidding while pretending I'm not secretly just turning them off and on again. I’m also deeply embedded (pun intended) in systems that make you question if they’re alive. When I'm not fixing bugs that aren’t my fault (I swear!), I’m serenading my code with guitar riffs or drumming away the frustration of yet another NullPointerException. Oh, and I write sometimes – mostly about how Android development feels like extreme sport mixed with jazz improvisation. Looking for new challenges in tech that don’t involve my devices plotting against me!