How to make an offline cache in android using Room database and MVVM architecture? 👨‍💻

Introduction:

No one likes the buffering animation. Android apps should have an offline cache for data that is fetched from the internet and is needed frequently. After all it also saves the internet bill of the user and the app can be used offline also with outdated data. Here in this article I’ll tell you how you can easily implement offline caching in your android app using the Room database.💯

⚠️This article contains concepts

If you’re using MVVM in your application, it will suit you best but you’ll at least get to know the concept even if you’re using some other architecture.

Let me first tell you briefly how we’ll do this. But before that I think I should put on a disclaimer that there is a lot of boilerplate code involved in this🥵. So don’t worry seeing all the classes🤗. We’ll:

But before starting, the question is why to use Room database only🤔

You may argue that I’ll rather use a local file system for my offline caching needs. Of Course you may do that but the file system is meant to store files like image, video, text, etc. and not the sequential data or tabular data. Another argument can be the use of shared preferences. They can be used to store data in form of key and value pairs but it also has a limitation that only a limited data can be stored in it.

So a room database is the best option. Some of the reasons why you should use it are ➛ it’s easy to set up,➛ using LiveData we can easily make our app with real time updates,➛ we can write queries in SQL and a lot more.

Architecture:

We have two sources of data at any given time. One is the room database stored inside the device and the other is the data fetched from the server. So the question is which to use to show in the UI❓ We create another class called the repository. All the data is fetched from the repository only. All the chaos of updating the local room database, etc. goes inside this repository only.

In the UI we show the data from the repository which shows the LiveData from the Room database. Simultaneously, we fetch the data from the API, i.e., from the server and update the Room database. Now the LiveData is changed and the observer gets called. This updates the UI.

Remember: Room database is the single source of truth

Concept of Get request:

🟠For the first time the repository is empty as the room database is empty and the API also hasn’t returned anything yet. So, till then we can show the loading dialog or something to let the user know that the data is being fetched.

🟠Now when the API returns the response successfully, we save the response in the Room database using the Insert query.

🟠The repository returns the LiveData object from the Room database which was empty till now. But now it has data in it. So the observer of the LiveData gets called.

🟠Finally, the UI is updated and the loading dialog is dismissed.

🟠This is what happens for the first time. For the rest of the times, we do exactly the same thing except that the loading dialog is not shown.

Concept of Put, Post or Delete request

When we update the data, say we want to insert row(s) or delete row(s), we update our server first using the API and then the Room database is updated. The reason we update the server first is hidden in the following question: Which is likely to fail more, Update request to the server or update request to the Room database❓ The answer is obvious: The update request to the server is more likely to fail because of a number of reasons like no internet connection, slow internet, high network traffic, etc.

So if we update the Room database first there is a high chance of discrepancy being created between the local and the remote database.

Lets see the steps now.

🟡We send the update request to the server and start showing the uploading dialog or something to let the user know that the data is being uploaded.

🟡Once the data is uploaded, stop showing the dialog.

🟡Now we also have to update the UI. Refresh the repository by following the same steps as in the get request.

🟡Again the LiveData gets changed, observer gets called and the UI is updated.

🟡Or you can also follow another approach which will save time and the internet but you’ll have to write a bit more code. You can update the Room database yourself once the server is updated.

Now let’s implement what we discussed

Step 1️⃣: Import the dependencies of Room database

You may refer this page to import all the dependencies.

You’ll also have to implement the coroutines. I am using data binding also which is optional.

Step 2️⃣: Make a data class to hold the data object

This is where you make all the columns for your SQL table. Here the table name is “cars_table”. You can use the annotations as given. There should be a primary key for which I have set autoGenerate=true. Rest all is simple. The names of the columns written inside parenthesis within inverted commas are the ones used by the table and the normal variable names are for our general purpose use.

package com.practice.offlinecaching

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName="cars_table")
data class Cars(
@PrimaryKey(autoGenerate = true)
var cardId:Long=0,

@ColumnInfo(name="image_url")
var imageURL:String?=null,

@ColumnInfo(name="name")
var name:String?=null,

@ColumnInfo(name="category")
var category:String?=null
)

Step 3️⃣: Make the DAO with suitable SQL queries

Data Access Object is the interface where we write our SQL queries. Different annotations are used here. Read this for further assistance.

Note that we are returning the LiveData of the list of Cars object.

Another important thing to note is the Insert query. We’re using onConflict = OnConflictStrategy.REPLACE

What this does is that in case of any conflict between the current row and the row that is being inserted, we replace the new row over the previous one. Read more about it here.

package com.practice.offlinecaching

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.room.*

@Dao
interface CarsDao
{
@Query("select * from cars_table where category= :key")
fun getCarsFromRoom(key:String): LiveData<List<Cars>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCarsToRoom(cars:List<Cars>)
}

Step 4️⃣: Make the database abstract class and the getDatabase function.

This is just a boilerplate code. The abstract class database is made with the entity of Database. This class simply represents the database and it inherits the RoomDatabase(). We’ll talk about this class some other time but now lets make another function that returns an instance of the database, getDatabase()

@Database(entities = [Cars::class],version=1,exportSchema = false)
abstract class CarsDatabase : RoomDatabase() {
abstract val CarsDatabaseDao: CarsDao
}

private lateinit var INSTANCE:CarsDatabase

fun getDatabase(context: Context):CarsDatabase
{
if(!::INSTANCE.isInitialized)
{
INSTANCE=
Room.databaseBuilder(context.applicationContext,CarsDatabase::class.java,"cars_database")
.fallbackToDestructiveMigration().build()

}
return INSTANCE
}

Step 5️⃣: Make the repository class

Repository is where we fetch data from the server and update our local Room database. In my case I am fetching the data in the refreshCars(String,String) method. Two parameters are just to tell you how we can send the parameters here. It is a suspend function because it is called from the viewModelScope. This is where the use of kotlin coroutines comes into play. It avoids the blocking of the UI thread while the data is being fetched from the server. ⌚️

Note that the class has a database variable in its constructor. It is used to insert the data fetched from the server to the Room database.

We also have a function to get the data from the Room database called the getCars(String) method. It also uses the database variable and returns a LiveData object that it gets back from the Room database.

package com.practice.offlinecaching

import android.os.AsyncTask
import android.util.Log
import androidx.lifecycle.LiveData
import com.androidnetworking.AndroidNetworking
import com.androidnetworking.common.Priority
import com.androidnetworking.error.ANError
import com.androidnetworking.interfaces.JSONObjectRequestListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject


class CarsRepository(private val database: CarsDatabase)
{

fun getCars(key:String?): LiveData<List<Cars>> {
return database.CarsDatabaseDao.getCarsFromRoom(key!!)
}

suspend fun refreshCars(type: String?,token:String?)
{
withContext(Dispatchers.IO){
var url=""
AndroidNetworking.get(url)
.setPriority(Priority.HIGH)
.build()
.getAsJSONObject(object : JSONObjectRequestListener {
override fun onResponse(response: JSONObject?) {
if (response != null) {


var carsListData = response.get("data") as JSONArray
val list = ArrayList<Cars>()
var i = 0
while (i < carsListData.length()) {
//Insert the data into the list
}
AsyncTask.execute { database.CarsDatabaseDao.insertCarsToRoom(list)
}
}
}
override fun onError(anError: ANError?) {}
})
}
}
}

Step 6️⃣: ViewModel class

An instance of the repository class is created and in the init block we fetch the LiveData object from the repository. The rest of the code depends upon your use case.

package com.practice.offlinecaching

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

class CarsViewModel(application: Application, val type: String?, val token:String?) : AndroidViewModel(
application)
{
private val viewModelJob= Job()
private val viewModelScope= CoroutineScope(viewModelJob + Dispatchers.Main)

private val database= getDatabase(application)
private val carsRepository=CarsRepository(database)

init {
viewModelScope.launch {
carsRepository.refreshCars(type,token)
}
}

val carsTypeList=carsRepository.getCars(type)

override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}

Step 7️⃣: Make the ViewModelFactory class

It has a simple boilerplate code. You must already know about this if you’re using the MVVM architecture. It returns the instance of ViewModel class.

package com.practice.offlinecaching

import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class CarsViewModelFactory(val app:Application, val type:String?, val token:String?) : ViewModelProvider.Factory {


override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CarsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return CarsViewModel(app,type,token) as T
}
throw IllegalArgumentException("Unable to construct viewmodel")
}
}

Just that and congratulations you have successfully implemented the offline caching. 🥳

For full code, see this repository: https://github.com/divyanshutw/Offline-caching

Githubhttps://github.com/divyanshutw

LinkedIn — https://www.linkedin.com/in/divyanshu-tiwari-7a7318173

👨‍💻 Android Developer||Java is ❤||Fond of Problem Solving||SSB Recommended