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

URLQuery TypeUsed For
https://login-flow-27d5.onrender.com/api/usersGETFetch all available user data
https://login-flow-27d5.onrender.com/api/usersPOSTCreate a new user (body of the request discussed later in the post)
https://login-flow-27d5.onrender.com/api/loginGETValidate 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

  1. 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.

  2. Request Customization: OkHttp allows for detailed customization of requests, including setting timeouts, adding headers, and more.

  3. Interceptors: OkHttp supports interceptors, which can be used to monitor, rewrite, and retry calls.

  4. WebSocket Support: OkHttp supports WebSocket communication, which facilitates real-time data exchange in a more efficient manner compared to HTTP polling.

  5. 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

  1. 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.

  2. Annotation-Based API Definitions: Retrofit allows developers to define APIs through annotations, which makes the code cleaner and easier to maintain.

  3. 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.

  4. Synchronous and Asynchronous Calls: Retrofit supports both synchronous and asynchronous network calls, allowing developers to choose the best approach for their needs.

  5. 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

  1. 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.

  2. 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.

  3. 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 the OkHttpClient. Interceptors are used to monitor, rewrite, and retry calls. In this block, we define an interceptor using a lambda expression where we get the ongoing chain.

  • 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 the build() method to create the OkHttpClient 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.
  • 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. The ResponseBody 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.

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 the create method of the Retrofit class to create an implementation of the WebService interface. The WebService::class.java syntax is used to get a Class object representing the WebService 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.

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 a Callback<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 the Callback 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: the Call object representing the request and the Response 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 the responseString variable.

    • Log.e("API_RESPONSE", "Success: $responseString"): Logs the successful response using the error log level (hence Log.e), although it's generally better to use a different log level such as Log.d or Log.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 the errorString 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: the Call object representing the request and a Throwable 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!