Implementing the Room Library with Jetpack Compose

Storing Data Locally on Android

Michihiro Iwasaki
10 min readMar 28, 2024

This article will guide you through the process of implementing the Room library in your Android application. It’s assumed that you have a basic understanding of Kotlin and Jetpack Compose. While our main objective is to implement the Room library, the explanations provided will be concise, focusing on implementation rather than in-depth code analysis. This approach is designed to help you integrate Room effectively without delving into every detail of the underlying code.

❓ What is the Room?

The Room library acts as an abstraction layer over SQLite, simplifying database management in Android applications. While direct usage of SQLite can lead to potential errors during SQL query execution, Room enhances safety and efficiency. It offers compile-time verification of SQL queries, significantly reducing the risk of errors.

It’s generally recommended to utilize the Room library for local data storage in SQLite, unless there are specific reasons not to. Room not only makes coding more robust and less error-prone but also streamlines the development process.

For comprehensive details on the Room library, visit the official documentation: Room Library Overview

🤖 Overview of the Sample App

Before we delve into implementing the Room library, let’s get acquainted with the sample app we’ll be developing: “My Friends App,” a simple utility for storing friends’ names.

The app includes the following UI components:

  • An input text field for entering names ..(1)
  • A save button to store the entered name ..(2)
  • A delete button to remove saved names ..(3)
  • An area to display saved data ..(4)

Here’s a preview of the app’s interface:

The image of screenshot that will be the app creating through this tutorial.

The app is intentionally designed with simplicity to keep the focus on the backend implementation rather than the UI. Therefore, while the UI elements are essential for interaction, our primary focus will be on integrating and utilizing the Room library to manage the app’s data.

In the upcoming section, we’ll start the hands-on process of integrating the Room library into our app!

⚙ Setting Up the Dependencies

To integrate the Room library, we need to configure the necessary dependencies in our build.gradle.kts files.

Project-Level Configuration: Open your project-level build.gradle.kts file and add the following code to enable Kotlin Symbol Processing (KSP), which is crucial for Room to analyze and generate code from annotations:

plugins {
/* other plugins settings */
id("com.google.devtools.ksp") version "1.9.23-1.0.19" apply false
}

Ensure that you’re using the latest KSP version available at the time of writing this article.

Module-Level Configuration: Next, navigate to the module-level build.gradle.kts file and incorporate the necessary dependencies for Room:

plugins {
/* other plugins settings */
id("com.google.devtools.ksp")
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.11"
}

dependencies {
/* other plugins settings */
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)

implementation(libs.androidx.lifecycle.viewmodel.compose)
}

Checking libs.versions.toml: Review your libs.versions.toml file to confirm or update the versions:

[versions]
agp = "8.3.0"
kotlin = "1.9.23"
coreKtx = "1.12.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.7.0"
activityCompose = "1.8.2"
composeBom = "2024.02.02"
lifecycleViewmodelCompose = "2.7.0"
roomCompiler = "2.6.1"
roomKtx = "2.6.1"
roomRuntime = "2.6.1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }

It’s crucial to ensure that the kotlinCompilerExtensionVersion is compatible with your Kotlin version. The latest version of kotlinCompilerExtensionVersion might not always support the most recent Kotlin release. Before proceeding, verify the compatibility of the Compose Compiler version with your Kotlin version at the official Android Developers site: Compose Compiler Release Notes

Validate Your Setup: After updating the dependencies, build your project to confirm there are no issues. If the build process fails, carefully examine the error messages. Common issues might include missing configurations or incorrect version numbers.

☛ Defining the Entity

As we begin implementing the Room library, let’s first set up our data structure. Create a data package to organize files related to Room, and within this package, create a file named MyFriendsData.kt. This file will define the entity for our Room database.

For our “My Friends” app, we need two columns in our database table: one for the ID (of type Int) and another for the friend's name (of type String). The ID will serve as a unique identifier for each row.

Here’s how you define the entity in MyFriendsData.kt:

/* MyFriendsData.kt */
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "sample")
data class MyFriend(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val name: String
)
  • The @Entity annotation is used to denote a Room entity. This annotation requires a table name, which is set to "sample" in our example.
  • The @PrimaryKey annotation marks a column as the primary key. Setting autoGenerate = true means that Room will automatically generate unique IDs for each entry.
  • Use the @ColumnInfo annotation to specify a custom column name. Here, the column for storing friend names is named "name."

For more detailed information on defining entities in Room, refer to the official documentation: Defining data using Room entities

☛ Defining the Dao

With the entity defined, the next step in implementing Room is to define the Data Access Object (Dao), which facilitates abstract interactions with our database. Through the Dao, we can simplify database operations.

For our “My Friends” app, we require three fundamental operations:

  1. Retrieving all data
  2. Inserting new data
  3. Deleting all data

Below is the example code for defining the Dao in MyFriendsDao.kt:

/* MyFriendsDao.kt */
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface MyFriendsDao {
@Query("SELECT * FROM sample")
fun getAll(): Flow<List<MyFriend>>

@Insert
suspend fun insertFriend(myFriend: MyFriend)

@Delete
suspend fun deleteAllMyFriends(allMyFriends: List<MyFriend>)
}
  • Begin the Dao definition with the @Dao annotation applied to the interface.
  • The getAll() method returns a Flow type. This does not require the suspend modifier because it provides a continuous stream of data.
  • Other methods, such as inserting or deleting data, should be marked with suspend to ensure they're executed asynchronously, respecting coroutine best practices.
  • The @Insert annotation simplifies the definition of data insertion methods. Similarly, the @Delete annotation is used for methods that remove data from the database.

For more advanced data manipulation methods or to explore further capabilities of Room’s Dao, consult the official documentation: Accessing data with Room

☛ Defining the Database Class

Now, we’ll establish the database class using the Room library. Let’s begin with an overview of the example code:

/* MyFriendsDatabase.kt */
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [MyFriend::class], version = 1)
abstract class MyFriendsDatabase: RoomDatabase() {
abstract fun myFriendsDao(): MyFriendsDao

companion object {
@Volatile
private var Instance: MyFriendsDatabase? = null

fun getMyFriendsDatabase(context: Context): MyFriendsDatabase {
return Instance ?: synchronized(this) {
Room.databaseBuilder(
context = context,
klass = MyFriendsDatabase::class.java,
name = "sample"
)
.build()
.also { Instance = it }
}
}
}
}
  • The @Database annotation is crucial. Here, you must list all entity classes in the entities parameter and define the database version. Database versions are essential for handling schema changes over time.
  • MyFriendsDatabase extends RoomDatabase, indicating it's a Room database class. Defined as an abstract class, Room takes care of its implementation.
  • The myFriendsDao() method exposes the Dao, enabling database operations through it.
  • The Instance variable, declared within a companion object, ensures that MyFriendsDatabase adheres to the singleton pattern, creating only one instance throughout the app.
  • Marking Instance with @Volatile guarantees that its value is always read from and written to the main memory, avoiding caching issues.
  • The getMyFriendsDatabase() function either returns the existing Instance or creates a new one within a synchronized block if it's null.

If this is your first encounter with implementing a Room database, the code might seem intricate. It’s not essential to grasp every detail immediately. Gradually deepening your understanding and consulting the official documentation when challenges arise can be very effective: Room Database Documentation

☛ Defining the Repository Class

Now, let’s create the MyFriendsRepository class. This class will serve as an intermediary between our database operations defined in the DAO and the UI or business logic of our application. By passing the DAO as a parameter to the repository's constructor, we enable direct interaction with the database through the DAO's methods.

/* MyFriendsRepository.kt */
class MyFriendsRepository(private val myFriendsDao: MyFriendsDao) {
fun getAll() = myFriendsDao.getAll()

suspend fun insertFriend(myFriend: MyFriend)
= myFriendsDao.insertFriend(myFriend)

suspend fun deleteAllMyFriends(allMyFriends: List<MyFriend>)
= myFriendsDao.deleteAllMyFriends(allMyFriends)
}

In the MyFriendsRepository, we define methods that correspond to the DAO's operations. The getAll method retrieves all friends' data, insertFriend adds a new friend, and deleteAllMyFriends removes all friends from the database. Notice the use of suspend for insertFriend and deleteAllMyFriends to support coroutines for asynchronous operations.

While we have established the MyFriendsRepository, instantiating it directly isn't feasible since it requires an instance of MyFriendsDao. The forthcoming section will address how to instantiate MyFriendsRepository with the necessary DAO.

☛ Solving Dependency Issues

To instantiate MyFriendsRepository, we require an instance of MyFriendsDao. This dependency chain necessitates a structured approach to ensure that all components are correctly instantiated. We'll address this by introducing a container class, MyFriendsContainer, which will manage the instantiation of MyFriendsRepository.

/* MyFriendsContainer.kt */
class MyFriendsContainer(private val context: Context) {
val myFriendsRepository: MyFriendsRepository by lazy {
MyFriendsRepository(MyFriendsDatabase.getMyFriendsDatabase(context).myFriendsDao())
}
}

The MyFriendsContainer class uses a lazy delegate to ensure that MyFriendsRepository is instantiated only when needed, using the appropriate Dao obtained from MyFriendsDatabase.

To supply the necessary context for our MyFriendsContainer, we'll create a custom Application class:

/* MyFriendsApplication.kt */
class MyFriendsApplication : Application() {
lateinit var container: MyFriendsContainer

override fun onCreate() {
super.onCreate()
container = MyFriendsContainer(this)
}
}

This Application class initializes MyFriendsContainer with the application context, ensuring it's available throughout the app.

To ensure our Application class is recognized, modify the AndroidManifest.xml:

/* AndroidManifest.xml */
<application
android:name=".MyFriendsApplication"
…other settings>

This configuration ensures that our custom application class is used, allowing us to access MyFriendsContainer across our application.

With this setup complete, we are now ready to integrate the Room database within our app’s architecture. The next section will focus on creating a ViewModel that utilizes MyFriendsRepository for data operations.

☛ Defining the ViewModel Class

While this article doesn’t delve deeply into UI management, we’ll briefly outline how to set up the HomeViewModel class, which interacts with our UI components. Assuming that our UI is defined in a Home composable within Home.kt, the corresponding HomeViewModel can be structured as follows:

/* HomeViewModel.kt */
class HomeViewModel(private val myFriendsRepository: MyFriendsRepository) : ViewModel() {
fun getAll(): Flow<List<MyFriend>>
= myFriendsRepository.getAll()

fun insertFriend(friendName: String) = viewModelScope.launch {
myFriendsRepository.insertFriend(MyFriend(name = friendName))
}

fun deleteAllMyFriends(allMyFriends: List<MyFriend>) = viewModelScope.launch {
myFriendsRepository.deleteAllMyFriends(allMyFriends)
}

companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as MyFriendsApplication)
HomeViewModel(application.container.myFriendsRepository)
}
}
}
}

This ViewModel class utilizes methods from MyFriendsRepository to perform data operations. The ViewModel is responsible for handling UI-related data operations and lifecycle-aware data manipulation. To solve the dependency issue, we provide a Factory within the ViewModel to ensure it's instantiated with the necessary repository.

With the ViewModel in place, we’re now poised to complete our sample app by connecting it with the Home composable.

☛ Creating the Home Composable

Now, let’s briefly explore the Home composable's structure. This code snippet serves as a basic framework, which you're encouraged to modify and enhance according to your app's requirements.

/* Home.kt */
@Composable
fun HomeView(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel(factory = HomeViewModel.Factory) // ..1
) {
val friendsList by viewModel.getAll().collectAsState(initial = emptyList()) // ..2
var friendNameInput by remember { mutableStateOf("") }

Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Display the list of friends
LazyColumn(
modifier = Modifier.weight(.7F),
verticalArrangement = Arrangement.Center
) {
items(friendsList) { friend ->
Card(
modifier = Modifier
.width(200.dp)
.height(80.dp)
.padding(vertical = 8.dp)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = friend.name, style = MaterialTheme.typography.displaySmall)
}
}
}
}

// Input field and buttons
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(.3F)
) {
OutlinedTextField(value = friendNameInput, onValueChange = { friendNameInput = it })
Button(onClick = { viewModel.insertFriend(friendNameInput) }) {
Text(text = "SAVE")
}
Button(onClick = { viewModel.deleteAllMyFriends(friendsList) }) {
Text(text = "ALL DELETE")
}
}
}
}
  • In line ..1, the HomeViewModel is instantiated using the custom factory we defined.
  • In line ..2, we retrieve and observe the list of friends from the database, updating the UI in real-time with any changes.

The following video demonstrates the app in action, showcasing data persistence even after the application is closed:

Congratulations! We’ve successfully built a simple app utilizing the Room library to manage local data storage.

<aside>
<p>
Thank you for reading!<br>
If you enjoyed this post, <br>
I'd appreciate a clap. 😄
</p>
</aside>

--

--