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(): UserDaoprovides access to the DAO.- The
getDatabasemethod 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
allUsersasLiveData. - Provides functions to insert and delete users, using
viewModelScopeto launch coroutines. - Includes a
UserViewModelFactoryto 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
AppDatabaseandUserDao. - Initialize the
UserViewModelusing theUserViewModelFactory. - Observe the
allUsersLiveDataand 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.