Android: Room, Saving Local Data the Modern Way

Anubhav
CodeX
Published in
5 min readMay 28, 2021
Photo by Franki Chamaki on Unsplash

Almost every application we use stores data for one purpose or another, be it to store images, files, user preferences, etc. One of the most common use cases is to cache relevant pieces of data so that when the device cannot access the network, the user can still browse that content while they are offline. Android is full of ways to store data, depending on our use case. In this article, I will be talking about the best-recommended way to store structured data, and that is the Room library.

The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.

Why should we be using Room over SQLite?

  • No runtime queries errors, as Room provides compile-time verification of SQL queries.
  • Convenience annotations that minimize repetitive and error-prone boilerplate code.
  • It integrates seamlessly with the other architecture components.

Primary Components

  • Entity
  • DAO
  • Database

Entity

Represents tables in your app’s database. Room creates a table for each class that has an @Entity annotation, the fields in the class correspond to columns in the table. Therefore, the entity classes are data classes that do not contain any logic.

DAO

DAOs are interfaces annotated with @Dao, they provide methods that your app can use to query, update, insert, and delete data in the database.

Database

The database class holds the database and serves as the main access point for the underlying connection to your app’s persisted data. The database class provides your app with instances of the DAOs associated with that database. In turn, the app can use the DAOs to retrieve data from the database as instances of the associated data entity objects.

Let’s walk through an example to understand the implementation.

To get started with Room, add the following dependencies to your app’s build.gradle file:

dependencies {
def roomVersion = "2.3.0"
implementation("androidx.room:room-runtime:$roomVersion")
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
androidTestImplementation "androidx.room:room- testing:$roomVersion"

}

The following code snippet defines a Recipe model, which contains a list of Results, wherein the Result class contains each recipe’s properties. We will be persisting a list of recipes in the database.

data class Recipe(
@SerializedName("results")
val results: List<Result>
)
data class Result(
@SerializedName("sourceName")
val sourceName: String,
@SerializedName("sourceUrl")
val sourceUrl: String,
@SerializedName("summary")
val summary: String,
@SerializedName("title")
val title: String,
@SerializedName("vegan")
val vegan: Boolean,
@SerializedName("vegetarian")
val vegetarian: Boolean,
@SerializedName("veryHealthy")
val veryHealthy: Boolean
)

Data Entity:

@Entity(tableName = "recipes_table")
class RecipesEntity(
var recipe: Recipe
) {

@PrimaryKey(autoGenerate = false)
var id: Int = 0

}

Our Entity class annotated with @Entity, has a name recipes_table. We will just have a single row of a list of recipes in the table, and so we do not need a primary key in this case and that is why I have set the autoGenerate property to false.

Data Access Object (Dao):

Our RecipesDao, annotated with @Dao, has two methods, one for inserting our recipes(RecipeEntity) in the table and the other for reading the cached recipes, which returns a flow of list of recipe entities.

@Dao
interface RecipesDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipes(recipesEntity: RecipesEntity)

@Query("SELECT * FROM recipes_table ORDER BY id ASC")
fun readRecipes(): Flow<List<RecipesEntity>>

}

Room by default can store primitive types only, to store custom objects we need to use TypeConverter to convert the custom objects into known database types. In our case, we will be converting our list of recipes into a string. The method which does the type conversion needs to be annotated with TypeConverter.

class RecipesTypeConverter {

val gson = Gson()

@TypeConverter
fun recipeToString(recipe: Recipe): String {
return gson.toJson(recipe)
}

@TypeConverter
fun stringToRecipe(recipeString: String): Recipe {
val objectType = object : TypeToken<Recipe>() {}.type
return gson.fromJson(recipeString, objectType)
}

}

I have defined two methods, one to convert our Recipe object into a string, and the other to convert back the string to the Recipe object. I am using Gson for the type conversion.

Database

@Database(
entities = [RecipesEntity::class],
version = 1,
exportSchema = true
)
@TypeConverters(RecipesTypeConverter::class)
abstract class RecipesDatabase : RoomDatabase() {

abstract fun recipesDao(): RecipesDao

}

Our database class annotated with @Database, defines the database configuration and serves as the app’s main access point to the persisted data. In the database, we need to mention the list of entities, the database version if we want to export the database schema into a folder, and the type convertors if any.

Usage:

After we have defined the data entity, the DAO, and the database object, we can use the following code to create an instance of the database. I am using Hilt for DI.

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

@Singleton
@Provides
fun provideDatabase(
@ApplicationContext context: Context
) = Room.databaseBuilder(
context,
RecipesDatabase::class.java,
RECIPES_DATABASE_NAME
).build()

@Singleton
@Provides
fun provideDao(database: RecipesDatabase) = database.recipesDao()

}

I have created a LocalDataSource class that will be exposing DAO’s methods to the repository. Here I am injecting the Dao class via constructor injection.

class LocalDataSource @Inject constructor(
private val recipesDao: RecipesDao
) {

suspend fun insertRecipes(recipesEntity: RecipesEntity) {
recipesDao.insertRecipes(recipesEntity)
}

fun readRecipes(): Flow<List<RecipesEntity>> {
return recipesDao.readRecipes()
}
}

Now in the repository, I will be injecting the Remote data source. I have used @ActivityRetainedScoped so that our repository survives configuration changes.

@ActivityRetainedScoped
class Repository @Inject constructor(
localDataSource: LocalDataSource
) {
val local = localDataSource
}

You can similarly inject your remote data in the repository and then use it in the ViewModel classes.

@HiltViewModel
class MainViewModel @Inject
constructor(
private val repository: Repository,
application: Application,
) :
AndroidViewModel(application) {

/**ROOM DATABASE*/

val readRecipes: LiveData<List<RecipesEntity>> = repository.local.readRecipes().asLiveData()

private fun insertRecipes(recipesEntity: RecipesEntity) {
viewModelScope.launch(Dispatchers.IO) {
repository.local.insertRecipes(recipesEntity)
}
}

private fun cacheRecipes(recipe: Recipe) { // saving recipe as a Recipe Entity object, and inserting in the db.
val recipesEntity = RecipesEntity(recipe)
insertRecipes(recipesEntity)
}
// Other logic }

Here I am reading data from the injected repository and storing it in the readRecipes variable as Live Data. I call cacheRecipes(recipe: Recipe) on a successful API response.

Finally, I am accessing the recipes in my fragment, and setting the data to the adapter,

private fun readCachedData() {
lifecycleScope.launch {
mainViewModel.readRecipes.observeOnce(viewLifecycleOwner) { dataBase ->
if (dataBase.isNotEmpty()) {
Log.d(TAG, "readCachedData:() ")

recipesAdapter.submitList(dataBase[0].recipe.results)
} else { // in case database is empty
requestApiData()
}
}
}
}

Our data in the database will look like this,

You can use the Database Inspector to visualise your data and run queries real time.

This is Room for you folks, hope it helped.

This post was inspired from one of the courses by Stefan Jovanovic on Udemy.

--

--

CodeX
CodeX

Published in CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Anubhav
Anubhav

No responses yet