Room is an Android persistence library that provides an abstraction layer over SQLite. Room makes working with databases relatively easy, even in Kotlin-based Android applications. This detailed lesson will cover:
Sections
1. Introduction to Room Database
Room is a part of Android Jetpack, and it provides an object-mapping layer over SQLite. It basically shields developers from the pain of database management, making possible operations on the database via Kotlin objects. Room provides compile-time checks of SQLite queries, hence preventing runtime errors.
2. Setting Up Room in Your Project
To use Room, you need to add the necessary dependencies to your build.gradle
file.
// build.gradle (app level) dependencies {
def room_version = "2.5.0" // Check for the latest version implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt implementation "androidx.room:room-ktx:$room_version" // For Kotlin extensions
}
- room-runtime: Te basic library with Room API for database management.
- room-compiler: This is required for compilation of the Room annotations into code at compile time.
- room-ktx: Kotlin-specific extensions for Room make it easier to work with coroutines.
Make sure you apply the Kotlin Kapt plugin at the top of your build.gradle
file:
apply plugin: 'kotlin-kapt'
3. Creating Entities
User
Entity
This data class represents a table in the Room database.
import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "users") // Specifies the table name data class User(
@PrimaryKey(autoGenerate = true) val id: Long = 0, // Primary key with auto-increment val name: String, // User's name val age: Int // User's age
)
@Entity
:This annotation tells Room that this class is an entity; i.e., it corresponds to a table.tableName
: The name of the table in the database.@PrimaryKey
: defines the id field as the primary key. The autoGenerate = true means Room will auto-increment this value when a new record is inserted.
4. Creating a DAO (Data Access Object)
UserDao
The DAO interface contains methods that Room uses to interact with the database.
import androidx.room.Dao import androidx.room.Insert import androidx.room.Query @Dao // Annotation indicating this is a DAO interface UserDao {
@Insert // Indicates that this method will insert a User into the database suspend fun insertUser(user: User)
@Query("SELECT * FROM users") // SQL query to select all users suspend fun getAllUsers(): List<User>
@Query("SELECT * FROM users WHERE id = :userId") // SQL query to select a user by ID suspend fun getUserById(userId: Long): User?
@Query("DELETE FROM users") // SQL query to delete all users suspend fun deleteAllUsers()
}
@Dao
: Marks this interface as a DAO.@Insert
: Room will create the necessary code to insert a User in the database.@Query
: Allows you to define SQL queries to fetch or manipulate data.
5. Creating the Database
AppDatabase
This abstract class defines the database configuration.
import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import android.content.Context @Database(entities = [User::class], version = 1) // Declares entities and version number abstract class AppDatabase : RoomDatabase {
abstract fun userDao(): UserDao // Abstract method to access UserDao companion object {
@Volatile private var INSTANCE: AppDatabase? = null fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) { // Thread-safe singleton pattern val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database" // Database name
).build()
INSTANCE = instance instance
}
}
}
}
@Database
: The annotation defines the entities that belong to the database and the version of the database.abstract fun userDao()
: UserDao Returns the UserDao that can be used to interact with the DAO methods.- Singleton Pattern: This implementation is performed in such a way that, during the app’s entire lifetime, just one instance of the database is ever created, thus preventing memory leak and ensuring thread safety.
6. Using Room in a Repository
UserRepository
The repository pattern abstracts the data sources for better separation of concerns.
class UserRepository(private val userDao: UserDao) {
suspend fun insert(user: User) {
userDao.insertUser(user) // Calls the DAO method to insert a user
}
suspend fun getAllUsers(): List<User> {
return userDao.getAllUsers() // Calls the DAO method to get all users
}
suspend fun getUserById(userId: Long): User? {
return userDao.getUserById(userId) // Calls the DAO method to get user by ID
}
suspend fun deleteAllUsers() {
userDao.deleteAllUsers() // Calls the DAO method to delete all users
}
}
UserRepository: This class will provide methods for accessing UserDao, hence encapsulating the data operation that will be done, providing easier testing and maintenance.
7. Example Application
MainActivity
This is where we use the ViewModel to interact with the database.
import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() {
private val userViewModel: UserViewModel by viewModels() // ViewModel instance override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Insert a user and fetch all users lifecycleScope.launch {
userViewModel.insert(User(name = "Alice", age = 25)) // Inserts a new user val users = userViewModel.getAllUsers() // Fetches all users users.forEach {
println(it) // Prints each user to the console
}
}
}
}
viewModels()
: A property delegate that provides an instance of the ViewModel scoped to the activity.lifecycleScope.launch
: Launches a coroutine tied to the lifecycle of the activity. This ensures that the coroutine is canceled if the activity is destroyed.
UserViewModel
This ViewModel handles the data for the UI.
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class UserViewModel(private val repository: UserRepository) : ViewModel() {
fun insert(user: User) {
viewModelScope.launch {
repository.insert(user) // Inserts the user using the repository
}
}
suspend fun getAllUsers(): List<User> {
return repository.getAllUsers() // Retrieves all users from the repository
}
}
- ViewModel: It survives configuration changes and is used to store UI-related data.
viewModelScope
: A CoroutineScope that is automatically canceled when the ViewModel is cleared.
ViewModel Factory (Optional)
If your ViewModel requires parameters, you need a factory.
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider class UserViewModelFactory(private val repository: UserRepository) : ViewModelProvider.Factory {
override fun create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
return UserViewModel(repository) as T // Create a UserViewModel instance
}
throw IllegalArgumentException("Unknown ViewModel class") // Exception for unknown ViewModel
}
}
ViewModelProvider.Factory: This interface is implemented to create ViewModel instances that require parameters.
Using the ViewModel Factory in Activity
class MainActivity : AppCompatActivity() {
private lateinit var userViewModel: UserViewModel override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val userDao = AppDatabase.getDatabase(application).userDao() // Get UserDao from the database val userRepository = UserRepository(userDao) // Create UserRepository userViewModel = ViewModelProvider(this, UserViewModelFactory(userRepository)).get(UserViewModel::class.java) // Get ViewModel instance lifecycleScope.launch {
userViewModel.insert(User(name = "Alice", age = 25)) // Insert a user val users = userViewModel.getAllUsers() // Fetch all users users.forEach {
println(it) // Print users
}
}
}
}
8. Testing Room Database
UserDaoTest
This test class verifies the functionality of the DAO.
import androidx.room.Room import androidx.test.core.app.ApplicationProvider import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test class UserDaoTest {
private lateinit var database: AppDatabase private lateinit var userDao: UserDao @Before fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build() // Creates an in-memory database for testing userDao = database.userDao() // Gets the UserDao
}
@After fun teardown() {
database.close() // Closes the database after tests
}
@Test fun testInsertAndGetUser() = runBlocking {
val user = User(name = "Alice", age = 25) // Create a test user userDao.insertUser(user) // Insert the user into the database val users = userDao.getAllUsers() // Retrieve all users assertEquals(users.size, 1) // Check that one user exists assertEquals(users[0].name, "Alice") // Check that the user's name is correct
}
}
JUnit Annotations:
@Before
: Runs before each test, setting up the in-memory database.@After
: Cleans up by closing the database after each test.@Test
: Marks a function as a test case.
runBlocking
: Runs a coroutine that blocks the current thread, allowing for the testing of suspend functions.
Conclusion
In this lesson, we have gone through the process with a full example of how to implement Room Database in a Kotlin Android application. Every part—starting from Entity and DAO to Repository and ViewModel—plays its role in maintaining data efficiently while keeping the app responsive.
This setup aids not only in managing the data but provides a clear structure by maintaining the principles of separation of concerns and architecture best practices. Any further questions or anything, you would like me to elaborate on, concerning the code above?.