Jetpack compose: How to search Room data using ViewModel

This code demonstrates a Jetpack Compose application that allows users to search for employees stored in a Room database. It utilizes ViewModel to manage the application's data and state.

Data Model and Room Integration:

  • The Employee class represents an employee record with id (primary key) and fullName attributes.
  • RoomSingleton is a Room database class that provides access to the database instance and the EmployeeDao interface.
  • EmployeeDao defines methods for interacting with the employee table: search, insert, insert all, and clear.

EmployeeViewModel:

  • EmployeeViewModel inherits from AndroidViewModel and manages the application's data lifecycle.
  • It holds a private _searchedEmployees mutable state flow to store the list of searched employees.
  • A public read-only searchedEmployees state flow exposes the search results to the UI.
  • The loadSearchedEmployees function retrieves employees from the database based on the search query and updates the _searchedEmployees state flow.
  • Other functions like insert, insertAll, and clear handle adding and removing employees from the database, triggering a search update afterward.

MainActivity with Jetpack Compose UI:

  • The MainContent composable function builds the UI for the application.
  • It utilizes ViewModel composition to access the EmployeeViewModel.
  • A text field allows users to enter a search term.
  • Buttons are provided to add 100 random employees or clear all employees from the database.
  • The UI displays the number of employees and a list of searched employees using a LazyColumn.

Summary:

This code showcases a well-structured implementation for searching Room data using ViewModel in a Jetpack Compose application. It demonstrates proper separation of concerns between the UI, data access layer, and data management logic. The use of state flows ensures a reactive and efficient UI update based on changes in the data.


MainActivity.kt

package com.example.composeroomexample

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { MainContent() }
    }
}


@Composable
fun MainContent(){
    val model = viewModel<EmployeeViewModel>()
    val textState = remember{ mutableStateOf("")}
    val list = model.searchedEmployees.collectAsState()

    Log.d("xapp", "Recomposition")

    Column(
        modifier = Modifier.padding(12.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(12.dp)
        ){
            // add new employees
            Button(onClick = {
                val employees = mutableListOf<Employee>()
                for (i in 1..100){
                    employees.add(
                        Employee(
                            null,UUID.randomUUID().toString()
                        )
                    )
                }
                model.insertAll(employees)
            }) { Text(text = "Add 100 Employees") }

            // delete all employees
            Button(onClick = { model.clear() }) {
                Text(text = "Clear")
            }
        }

        Text(
            text = if (textState.value.trim().isEmpty()){"Employees"}
            else {"Employees start with (${textState.value.trim()})"} +
                    " ${list.value.size}",
            fontWeight = FontWeight.Bold
        )

        Row() {
            TextField(
                value = textState.value,
                onValueChange = {
                    textState.value = it
                    model.loadSearchedEmployees(it.trim())
                },
                modifier = Modifier.fillMaxWidth(),
                placeholder = { Text(text = "Search employee")}
            )
        }

        LazyColumn(
            verticalArrangement = Arrangement.spacedBy(8.dp),
            contentPadding = PaddingValues(vertical = 12.dp)
        ) {
            itemsIndexed(list.value) { index,item ->
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    backgroundColor = Color(0xFFDCE2C9)
                ) {
                    Row(
                        modifier = Modifier.padding(12.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {

                        Box(
                            Modifier.size(48.dp).clip(CircleShape)
                                .background(Color(0xFF8DB600)),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(
                                text = "${item.id}",
                                style = MaterialTheme.typography.h6
                            )
                        }

                        Spacer(modifier = Modifier.width(12.dp))
                        Text(
                            text = item.fullName.take(12),
                            style = MaterialTheme.typography.h6
                        )
                    }
                }
            }
        }
    }
}
RoomSingleton.kt

package com.example.composeroomexample

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context

@Database(entities = [Employee::class], version = 1, exportSchema = false)
abstract class RoomSingleton : RoomDatabase() {
    abstract fun employeeDao():EmployeeDao

    companion object {
        private var INSTANCE: RoomSingleton? = null
        fun getInstance(context: Context): RoomSingleton {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context,
                    RoomSingleton::class.java,
                    "roomdb")
                    .build()
            }
            return INSTANCE as RoomSingleton
        }
    }
}
RoomEntity.kt

package com.example.composeroomexample

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "employeeTbl")
data class Employee(
    @PrimaryKey
    var id:Long?,

    @ColumnInfo(name = "uuid")
    var fullName: String
)
RoomDao.kt

package com.example.composeroomexample

import androidx.room.*

@Dao
interface EmployeeDao{
    @Query("SELECT * FROM employeeTbl WHERE uuid" +
            " LIKE:startsWith || '%' ORDER BY id DESC")
    fun searchEmployee(startsWith:String):List<Employee>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(employee:Employee)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(list:List<Employee>)

    @Query("DELETE FROM employeeTbl")
    suspend fun clear()
}
EmployeeViewModel.kt

package com.example.composeroomexample

import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch


class EmployeeViewModel(application:Application)
    : AndroidViewModel(application){

    private val db:RoomSingleton = RoomSingleton
        .getInstance(application)


    private val _searchedEmployees = MutableStateFlow(emptyList<Employee>())
    var searchedEmployees: StateFlow<List<Employee>> = _searchedEmployees

    init {
        loadSearchedEmployees("")
    }

    fun loadSearchedEmployees(startsWith: String) =  effect {
        _searchedEmployees.value = db.employeeDao().searchEmployee(startsWith)
        Log.d("xapp", "Loading employees")
    }

    fun insert(employee: Employee){
        viewModelScope.launch(Dispatchers.IO) {
            db.employeeDao().insert(employee)
            loadSearchedEmployees("")
        }
    }

    fun insertAll(list:List<Employee>){
        viewModelScope.launch (Dispatchers.IO){
            db.employeeDao().insertAll(list)
            loadSearchedEmployees("")
        }
    }

    fun clear(){
        viewModelScope.launch(Dispatchers.IO) {
            db.employeeDao().clear()
            loadSearchedEmployees("")
        }
    }

    private fun effect(block: suspend () -> Unit) {
        viewModelScope.launch(Dispatchers.IO) { block() }
    }
}
build.gradle [dependencies]

apply plugin: 'kotlin-kapt'

dependencies {
    def room_version = "2.4.2"
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha06"
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha06'
}
More android jetpack compose tutorials