This post is about how to build an Android app using RxJava, RxAndroid, Retrofit with MVVM pattern in Kotlin.
RxJava
RxJava is a Java implementation of Reactive Extension or ReactiveX. Basically, it’s a library that composes asynchronous events by following Observer Pattern.
The library offers a wide range of amazing operators like map, combine, merge, filter, and a lot more that can be applied to the data.
RxJava is all about two key components: Observable and Observer. In addition to these, there are other things like Schedulers, Operators, and Subscription.
- Observable: Observable is a data stream that does some work and emits data.
- Observer: Observer receives the data emitted by Observable.
- Subscription: The bonding between Observable and Observer is called a Subscription. There can be multiple Observers subscribed to a single Observable.
- Operator: Operators modify the data emitted by Observable before an observer receives them.
- Schedulers: Schedulers decides the thread on which Observable should emit the data and on which Observer should receive the data i.e background thread, main thread, etc.,
RxAndroid
RxAndroid is an extension of RxJava for Android which is used only in Android applications.
Schedulers are introduced in RxAndroid which plays a major role in supporting the multithreading concepts in android applications. Schedulers basically decide the thread on which a particular code runs whether on a background thread or main thread.
Even though there are a lot of Schedulers available, The below two Schedulers are extensively used in android programming.
- Schedulers.io(): This is used to perform operations like making network calls, reading disc/files, database operations, etc., This maintains a pool of threads.
- AndroidSchedulers.mainThread(): this provides us access to the main thread of the application to perform actions like updating the UI. We shouldn’t perform any intensive operations on this thread as ANR dialog can be thrown.
Creating new project
1 . Create a new project by going to File ⇒ New Android Project, select Empty Activity, provide app name, select language to kotlin and then finally click on finish.
2 . Open app-level build.gradle file and add the below changes:
- Add the below libraries under the dependencies section:
dependencies {
//ViewModel and livedata
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
//Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
implementation 'com.github.bumptech.glide:compiler:4.12.0'
// RxJava3
implementation 'io.reactivex.rxjava3:rxjava:3.1.3'
//RxAndroid
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
}
Note: While using RxAndroid in our project we still add the dependency of RxJava to work,the reason is, there might be a chance that RxAndroid doesn’t have the latest version of RxJava used in the project. So, using RxJava dependency we override the versioning of the internal RxJava version used in RxAndroid.
- Inside the android block, add viewBinding block with property enabled to true.
viewBinding {
enabled = true
}
- Finally, click on Sync Now.
3. Open the AndroidManifest.xml file and add the internet permission above the application element.
<uses-permission android:name="android.permission.INTERNET"/>
Setup the Data Layer
In the data layer, we have to prepare the model for the data, and an API call needs to be implemented.
Creating Model class
I will use “https://fake-movie-database-api.herokuapp.com/api?s=batman” API to fetch the data.
The API JSON response will be like this.
{
"Search": [
{
"imdbID": "tt0096895",
"Title": "Batman",
"Year": "1989",
"Poster": "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYwNjAyODIyMF5BMl5BanBnXkFtZTYwNDMwMDk2._V1_.jpg"
},
{
"imdbID": "tt0468569",
"Title": "The Dark Knight",
"Year": "2008",
"Poster": "https://ia.media-imdb.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_UX182_CR0,0,182,268_AL_.jpg"
},
....
]
}
For the response data, we need to create the below two model classes.
MovieList.kt
data class MovieList(@SerializedName("Search") val mList: List<Movie>)
Movie.kt
data class Movie(
@SerializedName("Title") val title: String,
@SerializedName("Poster") val poster: String,
val imdbID: String,
@SerializedName("Year") val year: String
)
Setting up Retrofit
Create an interface RetrofitService for the API call.
In the below code,the getAllMovies() method returns an Observable object of Type MovieList.
RetrofitService.kt
interface RetrofitService {
@GET("api?s=batman")
fun getAllMovies(): Observable<MovieList>
companion object {
var retrofitService: RetrofitService? = null
//Create the RetrofitService instance using the retrofit.
fun getInstance(): RetrofitService {
if (retrofitService == null) {
val retrofit = Retrofit.Builder()
.baseUrl("https://fake-movie-database-api.herokuapp.com/")
.addConverterFactory(GsonConverterFactory.create())
//You need to tell Retrofit that you want to use RxJava 3
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
retrofitService = retrofit.create(RetrofitService::class.java)
}
return retrofitService!!
}
}
}
Setup Data Repository
Inside the below repository class, we need to pass the retrofit service instance to perform the network call. The repository class will only interact with the network source, the response of the network call we will handle later in ViewModel.
MainRepository.kt
class MainRepository constructor(private val retrofitService: RetrofitService) {
fun getAllMovies() = retrofitService.getAllMovies()
}
Setup the ViewModel
The MainViewModel class extends the ViewModel.
In the ViewModel constructor, we need to pass the data repository to handle the response from the API call.
It uses LiveData to update the data to UI. LiveData only updates app component observers that are in an active lifecycle state.
MainViewModel.kt
package com.c1ctech.movieapprxjavaretrofit.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.c1ctech.movieapprxjavaretrofit.Movie
import com.c1ctech.movieapprxjavaretrofit.data.network.model.MovieList
import com.c1ctech.movieapprxjavaretrofit.data.repository.MainRepository
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observer
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
class MainViewModel(private val repository: MainRepository) : ViewModel() {
val movieList = MutableLiveData<List<Movie>>()
val errorMessage = MutableLiveData<String>()
lateinit var disposable: Disposable
fun getAllMovies() {
//observer subscribing to observable
val response = repository.getAllMovies()
response.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getMoviesListObserver())
}
private fun getMoviesListObserver(): Observer<MovieList> {
return object : Observer<MovieList> {
override fun onComplete() {
//hide progress indicator .
}
override fun onError(e: Throwable) {
movieList.postValue(null)
}
override fun onNext(t: MovieList) {
movieList.postValue(t.mList)
}
override fun onSubscribe(d: Disposable) {
disposable = d
//start showing progress indicator.
}
}
}
}
In the above code,
Inside getAllMovies() method,we get observable object as response from the API call in which we will apply the below methods:
- subscribeOn(Schedulers.io()): This tells the Observable to run the task on a background thread.
- observeOn(AndroidSchedulers.mainThread()): This tells the Observer to receive the data on the android UI thread so that you can take any UI-related actions.
- subscribe(): It takes observer as a parameter which receives the data emitted by Observable.
The getMoviesListObserver() method, returns Observer that listen to Observable. Observer provides the below interface methods to know the the state of Observable.
- onSubscribe(): Method will be called when an Observer subscribes to Observable. It contains a Disposable instance as parameter whose Disposable.dispose() can be called anytime to cancel the connection or dispose the subscription when an Observer no longer wants to listen to Observable. In android disposable are very useful in avoiding memory leaks.
- onNext(): This method will be called when Observable starts emitting the data.
- onError(): In case of any error, onError() method will be called.
- onComplete(): When an Observable completes the emission of all the items, onComplete() will be called.
ViewModel Factory
To create ViewModel we have ViewModelProviders utility provided by Android. But ViewModelProviders can only instantiate ViewModels with the no-arg constructor.
So to create a ViewModel with multiple arguments, we need to use a Factory that we can pass to ViewModelProviders to use when an instance of MyViewModel is required.
class MyViewModelFactory constructor(private val repository: MainRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
MainViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
Setting up the UI
In the UI part, We need to create an instance of the ViewModel and observe the API response. Based on the API response we need to update the UI.
Creating Activity Layout file
The below layout file contains a RecyclerView and a button (onClick of which we will fetch data from the API).
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="5"
tools:listitem="@layout/layout_rv_item" />
<Button
android:id="@+id/btnShowMovies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="30dp"
android:text="SHOW MOVIES"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Creating Adapter Layout file
The below layout file represents the UI of each item of recyclerView.
layout_rv_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:elevation="8dp"
app:cardCornerRadius="8dp">
<ImageView
android:id="@+id/moviePoster"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"/>
<TextView
android:id="@+id/movieTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/purple_700"
android:gravity="center_vertical"
android:padding="3dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/movieYear"
android:layout_width="match_parent"
android:layout_height="200dp"
android:text="Year: 1987"
android:textStyle="bold"
android:gravity="center"
android:textSize="25sp"
android:textColor="@android:color/holo_red_dark"/>
</androidx.cardview.widget.CardView>
Creating Adapter class
The MainAdapter is an adapter class for the recycler view to set all the items into recycler view.
MainAdapter.kt
package com.c1ctech.movieapprxjavaretrofit
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.c1ctech.movieapprxjavaretrofit.databinding.LayoutRvItemBinding
class MainAdapter : RecyclerView.Adapter<MainViewHolder>() {
var movies = mutableListOf<Movie>()
fun setMovieList(movies: List<Movie>) {
this.movies = movies.toMutableList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = LayoutRvItemBinding.inflate(inflater, parent, false)
return MainViewHolder(binding)
}
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val movie = movies[position]
holder.binding.movieTitle.text = movie.title
holder.binding.movieYear.text = "Year: " + movie.year
Glide.with(holder.itemView.context).load(movie.poster).placeholder(R.drawable.placeholder)
.into(holder.binding.moviePoster)
}
override fun getItemCount(): Int {
return movies.size
}
}
class MainViewHolder(val binding: LayoutRvItemBinding) : RecyclerView.ViewHolder(binding.root) {}
Creating MainActivity File
Inside MainActivity, we will observe the response from the API and update the UI.
MainActivity.kt
package com.c1ctech.movieapprxjavaretrofit.ui
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.c1ctech.movieapprxjavaretrofit.MainAdapter
import com.c1ctech.movieapprxjavaretrofit.databinding.ActivityMainBinding
import com.c1ctech.movieapprxjavaretrofit.data.network.RetrofitService
import com.c1ctech.movieapprxjavaretrofit.data.repository.MainRepository
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding
lateinit var viewModel: MainViewModel
private val retrofitService = RetrofitService.getInstance()
val adapter = MainAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//get viewmodel instance using ViewModelProvider.Factory
viewModel =
ViewModelProvider(this, MyViewModelFactory(MainRepository(retrofitService))).get(
MainViewModel::class.java
)
//set adapter in recyclerview
binding.recyclerview.adapter = adapter
binding.btnShowMovies.setOnClickListener {
viewModel.getAllMovies()
binding.btnShowMovies.visibility = View.GONE
binding.recyclerview.visibility = View.VISIBLE
}
//the observer will only receive events if the owner(activity) is in active state
//invoked when movieList data changes
viewModel.movieList.observe(this, Observer {
if (it != null) {
Log.d(TAG, "movieList: $it")
adapter.setMovieList(it)
} else {
Toast.makeText(this, "Error in fetching data", Toast.LENGTH_SHORT).show()
}
})
//invoked when a network exception occurred
viewModel.errorMessage.observe(this, Observer {
Log.d(TAG, "errorMessage: $it")
})
}
override fun onDestroy() {
//don't send events once the activity is destroyed
viewModel.disposable.dispose()
super.onDestroy()
}
}
When you run the app it will look like this: