Simplifying Network Requests in Kotlin with Retrofit and MVVM

Simplifying Network Requests in Kotlin with Retrofit and MVVM

ยท

7 min read

Introduction

What is MVVM and Why use MVVM?

What is Retrofit?

MVVM and Retrofit in Action

Introduction

Building and manufacturing an app is like building and manufacturing a Car. Just like when you build a car, you need to use different parts and tools to make it strong and fun to drive. In the world of apps, developers use special patterns and libraries to make their apps strong and work well. There are many patterns or architectures to build apps but here I will be focusing on MVVM.๐Ÿ˜‰

What is MVVM and Why use MVVM?

MVVM stands for Model-View-ViewModel (MVVM). In this architecture pattern

i] The Model handles data storage and retrieval.

ii] The View manages user interactions and displays information.

iii] The ViewModel serves as a mediator between the Model and View.

MVVM is used because the ViewModel components can be reused with different Views. MVVM also makes it easier to fix problems or make improvements in the app. We can test each part separately to make sure they work correctly.

So, by using MVVM in Android development, we make the app organized, easier to work with, and more reliable.๐Ÿ˜

What is Retrofit?

Retrofit is like a messenger that helps your app talk to the internet. Just like we humans talk to other humans using phones or computers. Similarly, your app needs to communicate with other computers to get information. So retrofit makes it easier to send requests and receive responses from those computers to your app.

You can think of retrofit as a translator which can convert the information sent from your app into a language that other computers can understand and then convert the response back into the language your app can understand and work with it.

For more information on retrofit, you can refer to Retrofit.

MVVM and Retrofit in Action

First, add the Internet Permission to the manifest file. This ensures that data retrieval issues are avoided when the app is built and launched. Or else, you know what happens.๐Ÿซ  If you're new to this, give it a try!๐Ÿคง

Add the following <uses-permission android:name="android.permission.INTERNET"/> permission in the manifest file as shown below.

Then let's add the required dependency for your app in the build.Gradle(module:app) file. The dependencies are provided below but make sure when you use it, you use the latest version of the dependencies.

//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")

// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")

//couriteins
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")

Now create four packages named Api, Models, Repository, and ViewModel. In

app-> java-> com.example.quotes as shown below.

For this project, we will be using the following Quotes API: https://type.fit/api/quotes. We will focus on GET requests for this blog post. For Post, Patch, Delete, and Put requests, I will write a separate blog post.

In the Models package, let's create a data class using the Kotlin-to-JSON plugin. To create the data class, copy the API response and paste it as shown below. Name it as QuotesList and Click Generate.

Moving on to the next step. Right-click on package Api -> add one Interface named ApiInterface or the name you prefer. And an object class named ApiUtilities as shown below.

Now in the ApiInterface class, write the below-provided code.

import com.example.quotes.Models.QuotesList
import com.example.quotes.Models.QuotesListItem
import retrofit2.Response
import retrofit2.http.GET

interface ApiInterface{
   @GET("quotes")
  suspend fun getQuotes():Response<QuotesList>
}

The explanation for the code is given below :)

1] interface ApiInterface: We define an interface named ApiInterface to encapsulate our API methods.

2] @GET("quotes"): This annotation signifies that we are making a GET request to the "quotes" endpoint. Adjust the endpoint URL according to the API you're integrating.

3] suspend fun getQuotes() Represent an API call to retrieve quotes. The suspend modifier means that this modifier can be called from the coroutine, which can be paused and resumed later without blocking the thread.

4] The Response<QuotesList> Return type means that we can expect the API response to contain a list of quotes, where each quote will be represented as an instance of the QuotesList model class. The attributes of the QuotesList model class might include author, content, and year.

Add the following code to the ApiUtilities class in the Object file. This code contains a function that returns an instance of the Retrofit Object.

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ApiUtilites {
val BASE_URL = "https://type.fit/api/"//defining baseurl

    //the below function will return the retrofit instance
     fun getRetrofitInstance(): Retrofit {
         return Retrofit
             .Builder()//This creates an instance of Retrofit.Builder class
             .baseUrl(BASE_URL)//This sets the base URL for all the API endpoints
             .addConverterFactory(GsonConverterFactory.create())//this convert the response data into JSON using the Gson library.
             .build()//This builds the Retrofit instance with the configured settings and returns the final instance.
       }
}

Now let's move on to the Repository package and create a class named QuotesRepository which takes the ApiInterface object as a parameter as shown below followed by the explanation of the code.

The class contains a MutableLiveData object named mutableLiveData, which is of type QuotesList. MutableLiveData is a subclass of LiveData which allows us to set values. The quotesLiveData property is an immutable LiveData object that provides read-only access to mutableLiveData from other classes.

The getquotesfromQuotesRepository function is a suspend function that is used to fetch quotes data from the API. It calls the getQuotes method from the apiInterface object, which returns a response object (result). If the response body is not null, it sets the value of mutableLiveData using the postValue method.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.quotes.Api.ApiInterface
import com.example.quotes.Models.QuotesList


class QuotesRepository(private val apiInterface: ApiInterface) {
    private val mutableLiveData = MutableLiveData<QuotesList>()

    val quotesliveData: LiveData<QuotesList>
        get() = mutableLiveData

    suspend fun getquotesfromQuotesRepository() {
        val result = apiInterface.getQuotes()
        if (result.body() != null) {
            mutableLiveData.postValue(result.body())
        }
    }


}

Here, note how data is flowing and going to views to update the user interface(UI). there is a block diagram at the end of the blog.

The QuotesViewModel class takes a QuotesRepository object as a constructor parameter. When an instance of the QuotesViewModel is created, and the init block is called and launches a coroutine in the IO context using viewModelScope.launch(Dispatchers.IO). Within this coroutine, the getquotesfromQuotesRepository() function of the quotesRepository is called to fetch quote data from the API. This ensures that the API call is made in the background thread to avoid blocking the UI.

class QuotesViewModel(private val quotesRepository: QuotesRepository) : ViewModel() {
    init {
        viewModelScope.launch(Dispatchers.IO) {
          quotesRepository.getquotesfromQuotesRepository()
        }
    }         

  val quotes :LiveData<QuotesList>
        get() = quotesRepository.quotesliveData 


}

The quotes property is a read-only LiveData object that is exposed to the UI for observing the quotes data. It is derived from the quotesliveData property of the quotesRepository. This means that whenever the quotesliveData property in the quotesRepository is updated, the quotes property will automatically reflect the changes.

Now make another class in the same ViewModel Package named quotesViewModelFactory. We make this class because we are giving a quotesRepository object as a parameter to the QuotesViewModel constructor. Inside the create method, a new instance of QuotesViewModel is created, passing the quotesRepository as a parameter. Since the create method is generic, it casts the created QuotesViewModel to type T before returning it.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.quotes.Repository.QuotesRepository

class QuotesViewModelFactory(private val quotesRepository: QuotesRepository):ViewModelProvider.Factory{
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return QuotesViewModel(quotesRepository) as T
    }
}

Next moving on to MainActivity.kt file, this class utilizes the QuotesViewModel and QuotesViewModelFactory to retrieve and observe a list of quotes as shown below.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.ViewModelProvider
import com.example.quotes.Api.ApiInterface
import com.example.quotes.Api.ApiUtilites
import com.example.quotes.Repository.QuotesRepository
import com.example.quotes.ViewModel.QuotesViewModel
import com.example.quotes.ViewModel.QuotesViewModelFactory


class MainActivity : AppCompatActivity() {
    lateinit var quotesViewModel: QuotesViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val apiInterface = ApiUtilites.getRetrofitInstance().create(ApiInterface::class.java)
        val quotesRepository = QuotesRepository(apiInterface)
        quotesViewModel = ViewModelProvider(this,QuotesViewModelFactory(quotesRepository)).get(QuotesViewModel::class.java)

        quotesViewModel.quotes.observe(this,{
            Log.d("DATA", "Authors: ${it.map { it.text }}")
        })
    }
}

First, define lateinit var quotesViewModel: QuotesViewModel. The lateinit keyword signifies that the property will be assigned a value later in the code, prior to being used. Next, create an instance of ApiInterface using val apiInterface = ApiUtilites.getRetrofitInstance().create(ApiInterface::class.java), and pass this instance to the quotesRepository object as indicated above.

The ViewModelProvider is used to create an instance of QuotesViewModel by passing the this reference (referring to the MainActivity instance) and a QuotesViewModelFactory instance as parameters.

quotesViewModel.quotes.observe(this, { quotes -> ... }) sets up an observer on the quotes LiveData property of quotesViewModel.

When the value of the quotesLiveData changes, the lambda function { quotes -> ... } is executed.

Inside the lambda, the authors' texts of the quotes are logged using Log.d("DATA", "Authors: ${quotes.map { it.text }}").

Voila, it's done. And in the below block diagram, you can see the flow of how each class is interacting.

If you find this article helpful, have a question or spot an error/typo, please leave a comment in the comment section below. And thanks to my friends Dr.GoodDay, Sai, and ChatGPT for helping me with this.๐Ÿ’ซ

ย