Retrofit with ViewModel in Kotlin (Part-1)

Krishna sundar
Developer Community SASTRA
4 min readMar 7, 2022

Retrofit: Library to make API calls.

ViewModel: Encapsulates the data for a UI controller to let the data survive configuration changes.

I am going to split this article into 2 parts.

Source Code: https://github.com/krish-dev-7/sitMySeat

Part 1: Implementing Retrofit.

Part 2: Implementing ViewModel.

Add the following permission in the android manifest:

<uses-permission android:name="android.permission.INTERNET" />

Add the following plugins:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

Add the following dependencies:

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
//okhttp
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.1'
//logging interceptor
implementation "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.1"

//Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

// Coroutine Lifecycle Scopes
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
// Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'

API URL: https://api.themoviedb.org/3/list/1?api_key=<YOUR_API_KEY>

To add Constants and Resource file, create a separate package.

In this case , app →utils →Constants.kt :

package com.example.sitmyseat.utilsclass Constants {
companion object {
const val API_KEY = "***API KEY***" //Put your api key here
const val BASE_URL = "https://api.themoviedb.org"
}
}

app →utils →Resource.kt :

package com.example.sitmyseat.utilssealed class Resource<T>(
val data: T? = null,
val message: String? = null
) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T> : Resource<T>()
}

Keeping these constants in the Constant class and using the Resource class as the wrapper for your response is a really good practice!

  1. Sealed class is an abstract class where you can have a defined number of inner classes.
  2. Create separate classes for each type of response to handle it i.e. Success, Error and Loading.

Steps:

  1. Data class as Model for the response.
  2. Interface for the function calls.
  3. Retrofit Instance .
  4. Recycler Views and Adapters.

Step 1 (Model class file):

I am using a plugin from the Android studio for converting a JSON response into a Kotlin data class.

JSON to Kotlin class plugin

MovieResponse.kt file:

package com.example.sitmyseat.models

data class MovieResponse(
val created_by: String,
val description: String,
val favorite_count: Int,
val id: String,
val iso_639_1: String,
val item_count: Int,
val items: List<Item>,
val name: String,
val poster_path: String
)

item.kt file:

package com.example.sitmyseat.models

data class Item(
val adult: Boolean,
val backdrop_path: String,
val genre_ids: List<Int>,
val id: Int,
val media_type: String,
val original_language: String,
val original_title: String,
val overview: String,
val popularity: Double,
val poster_path: String?,
val release_date: String,
val title: String,
val video: Boolean,
val vote_average: Double,
val vote_count: Int


) {
override fun toString(): String {
return "adult : $adult" +
"\noriginal_language : '$original_language'" +
"\npopularity:$popularity" +
"\nposter_path : '$poster_path'" +
"\nrelease_date='$release_date'" +
"\ntitle='$title'" +
"\nvote_count=$vote_count"
}
}

Step 2 (API Interface):

Create an Interface to inform Retrofit about the following functions,so to access the data.

MovieApi.kt file:

package com.example.sitmyseat.api

import com.example.sitmyseat.models.MovieResponse
import com.example.sitmyseat.utils.Constants.Companion.API_KEY
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface MovieApi {

@GET("3/list/1")
suspend fun getMovies(
@Query("api_key")
key: String =API_KEY
): Response<MovieResponse>

}

Explanation:

    @GET("3/list/1")

→ This annotates the Retrofit that getMovies() is the function for the endpoint i.e. “3/list/1”.

suspend fun getMovies

→ Use the suspend keyword to make this function run in co-routines, which runs in the background thread without spamming the main thread.

@Query("api_key")

→ Annotate the Retrofit to pass the query parameter in the code i.e. key: String is passed.

Step 3 (Retrofit Instance):

  1. Add Interceptors to check the current status of the network calls.
val interceptor= HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

2. Add Client with the interceptor to make your job easy.

val client = OkHttpClient.Builder().addInterceptor(interceptor).build()

3. Create the Retrofit builder by adding Gson convertor factory to convert JSON to the required object type.

Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client).build()

4. The following line will create the API request.

retrofit.create(MovieApi::class.java)

The RetroFitInstance.kt file:

package com.example.sitmyseat.api

import com.example.sitmyseat.utils.Constants.Companion.BASE_URL
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class RetroFitInstance {
companion object {
val retrofit by lazy {
val interceptor = HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

val client = OkHttpClient.Builder().addInterceptor(interceptor).build()

Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client).build()

}
val api by lazy {
retrofit.create(MovieApi::class.java)
}
}
}

→The Companion object is like a static class, which creates the object only once.

→Create variables “retrofit” and “api” to separate the builder and the API call.

Step 4 (RecyclerView and Adapter Class):

→ Now we have to use the diffUtil method to create an adapter, which is used to reload the newly added item, rather than the whole list.

→ We are going to use the details available in the Item model and not the whole MovieResponse model.

  1. Glide is used to display the network image.
Glide.with(this).load("https://image.tmdb.org/t/p/original${movie.poster_path}").into(ivArticleImage)

ivArticleImage is the ID of ImageView, from XML

Layout files will be present in the source code pasted below.

ivArticleImage

MoviesAdapter.kt :

package com.example.sitmyseat.adapters

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.sitmyseat.R
import com.example.sitmyseat.models.Item
import kotlinx.android.synthetic.main.item_movie.view.*

class MoviesAdapter : RecyclerView.Adapter<MoviesAdapter.MListHolder>() {

inner class MListHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

private val differCallBack = object : DiffUtil.ItemCallback<Item>(){
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id==newItem.id
}

override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}

}

val differ = AsyncListDiffer(this, differCallBack)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MListHolder {
return MListHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_movie,
parent,
false
)
)
}

override fun onBindViewHolder(holder: MListHolder, position: Int) {
val movie = differ.currentList[position]
holder.itemView.apply {
movie.poster_path?.let{
Glide.with(this).load("https://image.tmdb.org/t/p/original${movie.poster_path}").into(ivArticleImage)
}
textView.text = movie.title
textView2.text = movie.toString()
}
}

override fun getItemCount(): Int {
return differ.currentList.size
}
}

This is the end of the Retrofit section. Checkout the Part 2 for the ViewModel implementation to get the complete working model of the app.

Part 2 : https://medium.com/dsc-sastra-deemed-to-be-university/retrofit-with-viewmodel-in-kotlin-part-2-15f395e32424

Source code: https://github.com/krish-dev-7/sitMySeat

--

--