Most of our app nowadays are mean to display some data that retrieve from backend or internet. It became an expensive cost if every time user open the app, the app must fetch the data directly from server. One of the solution is that app has its own database and fetch from server periodically.
This is something that I learned from my project, on how to convert a fully online app to an app that used offline/cache database.
Supposed that you have an app fully online like the app bellow (source code). Which fetch news data about crypto from api and display it every time user open the app or refresh the app.

- Create database & ViewModel
To make things easier, I had assume that Hilt Dependency Injection already properly set-up. Create offline database based on MVVM pattern.
*Model/POJO to be converted to entity. Don't forget to add PrimaryKey if it havent so
@Entity(tableName = "news_table")
data class News (
@SerializedName("title") var title: String? = null,
@SerializedName("url") var url: String? = null,
@SerializedName("source") var source: String? = null,
@PrimaryKey(autoGenerate = true) var id: Int
)
*Add dao to the database
@Dao
interface NewsDao {
@Query("DELETE FROM news_table")
fun deleteAll()
@Insert
fun insertAll(newsList: List<News?>?)
@Query("SELECT * from news_table ORDER BY id ASC")
fun getAllNews(): LiveData<List<News>>
}
*Add database to the database system
@Database(entities = [News::class], version = 1, exportSchema = true)
abstract class NewsDB : RoomDatabase() {
abstract fun newsDao(): NewsDao
companion object {
@Volatile
private var instance: NewsDB? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also { instance = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
NewsDB::class.java,
"news.db"
).build()
}
}
*Add repository to the database system. Pay attention on what function that we will need for this system. On this app, it will need fetch data, save data to db, display data from database and delete data from database
class NewsRepository @Inject constructor(
val api: RetrofitAPI,
val dao: NewsDao
) {
suspend fun fetchNewsFromNet() =
api.fetchNews()
fun insertNewsToDB(list: List<News>) =
dao.insertAll(list)
fun fetchNewsFromDB() =
dao.getAllNews()
fun deleteNewsFromDB() =
dao.deleteAll()
}
*Add viewModel to the system
@HiltViewModel
class NewsViewModel @Inject constructor(
private val repository: NewsRepository
) :
ViewModel() {
val newsList = MutableLiveData<List<News>>()
var job: Job? = null
var loading = MutableLiveData<Boolean?>()
override fun onCleared() {
job?.cancel()
super.onCleared()
}
}
2. Remove the function from Fragment to viewModel
After we had set up all the database system, now we can migrate the retrieve data function from Fragment to ViewModel. On the Fragment now we can called the function from viewModel.
@AndroidEntryPoint
class NewsFragment : Fragment() {
...
private val viewModel by activityViewModels<NewsViewModel>()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
viewModel.fetchDataFromNet()
viewModel.newsList.observe(viewLifecycleOwner, {
adapter.setData(it)
})
viewModel.loading.observe(viewLifecycleOwner, {
if (it == true){
binding.loadingBar.visibility = View.VISIBLE
} else {
binding.loadingBar.visibility = View.INVISIBLE
binding.refreshLayout.isRefreshing = false
}
})
binding.refreshLayout.setOnRefreshListener {
viewModel.fetchDataFromNet()
}
}
...
}@HiltViewModel
class NewsViewModel @Inject constructor(
private val repository: NewsRepository
) :
ViewModel() {
val newsList = MutableLiveData<List<News>>()
var job: Job? = null
var loading = MutableLiveData<Boolean?>()
fun fetchDataFromNet() {
job = CoroutineScope(Dispatchers.IO).launch {
loading.postValue(true)
val list = repository.fetchNewsFromNet()
withContext(Dispatchers.Main){
if (list.isSuccessful){
newsList.postValue(list.body())
}
loading.postValue(false)
}
}
}
...
}
3. Test the app again, ensure it is working properly.
Now if we run our app, there is not much change from the previous code on tis display. But it lay a good foundation for the next step.