In modern Android development, managing local data efficiently is crucial. Room Persistence Library, part of Android Jetpack, offers an abstraction layer over SQLite, simplifying database operations. This blog post delves into integrating Room Database with an XML-based UI in a Kotlin project.
What is Room Persistence Library?
Room is a persistence library that provides an abstraction layer over SQLite. It allows you to interact with an SQLite database as if you were working with regular Kotlin data classes. Key benefits of using Room include:
- Compile-time verification of SQL queries: Helps catch errors early.
- Convenient annotations: Simplifies database interactions.
- Integration with LiveData and Flow: Enables reactive data streams.
Why Integrate Room with XML UI?
- Backward Compatibility: Maintains support for older projects using XML layouts.
- Gradual Migration: Facilitates a step-by-step transition from XML to Jetpack Compose.
- Flexibility: Allows developers to leverage existing XML knowledge while adopting Room for data management.
How to Integrate Room Database with XML UI in Kotlin
To integrate Room Database with an XML-based UI, follow these steps:
Step 1: Add Dependencies
First, add the necessary Room dependencies to your build.gradle
file:
dependencies {
implementation "androidx.room:room-runtime:2.6.1"
kapt "androidx.room:room-compiler:2.6.1"
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.6.1"
// Lifecycle dependencies
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
}
// Add kapt plugin
plugins {
id 'kotlin-kapt'
}
Remember to apply the kotlin-kapt
plugin to enable annotation processing for Room.
Step 2: Define the Entity
Create a data class annotated with @Entity
to represent the database table:
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val firstName: String,
val lastName: String
)
Here, @Entity(tableName = "users")
specifies the table name, and @PrimaryKey
indicates the primary key. autoGenerate = true
allows Room to automatically generate unique IDs.
Step 3: Create the Data Access Object (DAO)
Define a DAO interface to specify the database operations:
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import androidx.room.Delete
import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("SELECT * FROM users")
fun getAllUsers(): Flow>
@Query("SELECT * FROM users WHERE id = :id")
fun getUserById(id: Int): Flow
}
The @Dao
annotation marks this interface as a data access object. Annotations like @Insert
, @Update
, @Delete
, and @Query
define the database operations. The use of Flow
from Kotlin Coroutines enables asynchronous and reactive data streams.
Step 4: Build the Room Database
Create an abstract class annotated with @Database
:
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
In this class:
@Database(entities = [User::class], version = 1, exportSchema = false)
specifies the entities, version, and export schema settings.abstract fun userDao(): UserDao
provides access to the DAO.- The
getDatabase
method uses the Singleton pattern to ensure only one instance of the database is created.
Step 5: Implement ViewModel
Create a ViewModel to manage the UI-related data:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val allUsers = userDao.getAllUsers().asLiveData()
fun insertUser(firstName: String, lastName: String) {
viewModelScope.launch {
val user = User(firstName = firstName, lastName = lastName)
userDao.insert(user)
}
}
fun deleteUser(user: User) {
viewModelScope.launch {
userDao.delete(user)
}
}
class UserViewModelFactory(private val userDao: UserDao) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return UserViewModel(userDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
Here, the ViewModel:
- Exposes
allUsers
asLiveData
. - Provides functions to insert and delete users, using
viewModelScope
to launch coroutines. - Includes a
UserViewModelFactory
to instantiate the ViewModel with the DAO.
Step 6: Design the XML Layout
Create the XML layout for your activity:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<EditText
android:id="@+id/firstNameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="First Name"
android:inputType="text" />
<EditText
android:id="@+id/lastNameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Last Name"
android:inputType="text" />
<Button
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add User" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/usersRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/user_item" />
</LinearLayout>
This layout includes EditText fields for first and last names, a button to add users, and a RecyclerView to display the list of users.
Step 7: Create RecyclerView Adapter and Layout
Define a simple layout (user_item.xml
) for displaying user information:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/userNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
Create an adapter (UserAdapter.kt
) to populate the RecyclerView:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class UserAdapter(private val users: List<User>) : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val userNameTextView: TextView = itemView.findViewById(R.id.userNameTextView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.user_item, parent, false)
return UserViewHolder(itemView)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val currentUser = users[position]
holder.userNameTextView.text = "${currentUser.firstName} ${currentUser.lastName}"
}
override fun getItemCount(): Int {
return users.size
}
}
Step 8: Connect Room and ViewModel to the UI (MainActivity)
Finally, connect the Room database and ViewModel to your activity:
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var userViewModel: UserViewModel
private lateinit var firstNameEditText: EditText
private lateinit var lastNameEditText: EditText
private lateinit var addButton: Button
private lateinit var usersRecyclerView: RecyclerView
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize UI elements
firstNameEditText = findViewById(R.id.firstNameEditText)
lastNameEditText = findViewById(R.id.lastNameEditText)
addButton = findViewById(R.id.addButton)
usersRecyclerView = findViewById(R.id.usersRecyclerView)
// Initialize RecyclerView
usersRecyclerView.layoutManager = LinearLayoutManager(this)
// Get instance of the database
val database = AppDatabase.getDatabase(this)
val userDao = database.userDao()
// Initialize ViewModel
userViewModel = ViewModelProvider(this, UserViewModel.UserViewModelFactory(userDao))
.get(UserViewModel::class.java)
// Observe LiveData
userViewModel.allUsers.observe(this) { users ->
adapter = UserAdapter(users)
usersRecyclerView.adapter = adapter
}
// Set click listener for the add button
addButton.setOnClickListener {
val firstName = firstNameEditText.text.toString()
val lastName = lastNameEditText.text.toString()
userViewModel.insertUser(firstName, lastName)
firstNameEditText.text.clear()
lastNameEditText.text.clear()
}
}
}
In the MainActivity
:
- Initialize UI elements, the RecyclerView, and its adapter.
- Get an instance of the
AppDatabase
andUserDao
. - Initialize the
UserViewModel
using theUserViewModelFactory
. - Observe the
allUsers
LiveData
and update the RecyclerView adapter whenever the data changes. - Set a click listener on the add button to insert new users into the database via the ViewModel.
Conclusion
Integrating Room Database with XML UI in Kotlin provides a robust and efficient way to manage local data while maintaining backward compatibility. By following these steps, developers can create a reactive and maintainable application. Room simplifies database operations and promotes cleaner code architecture, making it a valuable tool for Android development.