Retrofit with ViewModel in Kotlin (Part-1)
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!
- Sealed class is an abstract class where you can have a defined number of inner classes.
- Create separate classes for each type of response to handle it i.e. Success, Error and Loading.
Steps:
- Data class as Model for the response.
- Interface for the function calls.
- Retrofit Instance .
- 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.
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):
- 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.
- 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.
Source code: https://github.com/krish-dev-7/sitMySeat