Room Database Integration with XML UI in Kotlin

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 as LiveData.
  • 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 and UserDao.
  • Initialize the UserViewModel using the UserViewModelFactory.
  • 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.