Jetpack compose: ViewModel Room add insert data

Introduction

This code demonstrates a Jetpack Compose application that utilizes Room for persistent data storage and manages employee data using a ViewModel. The user interface (UI) allows adding single or multiple employees, displaying a list of employees, and clearing the data.

Breakdown

The code is divided into several parts:

  1. MainActivity.kt: This file defines the main activity of the application. It sets the content of the activity using a composable function MainContent.

  2. MainContent.kt: This composable function builds the UI of the app. It uses various Jetpack Compose components like Column, Row, Button, LazyColumn, and Card to display buttons, text, and a list of employees.

    • It utilizes a viewModel composable to access the EmployeeViewModel.
    • It defines a state variable textState to hold potential filter text for the employee list.
    • It retrieves the list of employees from the ViewModel using collectAsState and logs a message on recomposition.
    • It displays two buttons, one to add 10 random employees and another to add a single random employee.
    • It displays a button to clear the employee data and a text showing the number of employees, potentially filtered by textState.value.
    • It uses LazyColumn to display a list of employees in cards. Each card shows the employee's ID and name (truncated to 12 characters).
  3. RoomSingleton.kt: This file defines a singleton class RoomSingleton that acts as a central access point to the Room database. It uses Room's database builder to create an instance of the database and provides a method to get the instance.

  4. RoomEntity.kt: This file defines a data class Employee that represents an employee with an ID (Long) and a full name (String). It is annotated with @Entity to indicate it's a Room entity stored in a table named "employeeTbl".

  5. RoomDao.kt: This file defines an interface EmployeeDao that interacts with the Room database for employee data. It defines methods for:

    • Retrieving all employees (allEmployees).
    • Inserting a single employee (insert).
    • Inserting a list of employees (insertAll).
    • Clearing all employees (clear).
  6. EmployeeViewModel.kt: This class defines a ViewModel named EmployeeViewModel that manages the employee data and interacts with the Room database.

    • It retrieves the instance of RoomSingleton using the application context.
    • It defines a _Employees state flow to hold the list of employees.
    • It exposes a employees state flow as a read-only view of _Employees.
    • In the init block, it calls loadEmployees to fetch employees from the database.
    • loadEmployees is a suspend function launched in a coroutine scope to fetch employees and update _Employees.
    • It provides functions for inserting a single employee (insert), inserting a list of employees (insertAll), and clearing all employees (clear). These functions launch coroutines to interact with the database and update _Employees after the operation.
    • The effect function is a helper function used to launch coroutines in the ViewModel scope.
  7. build.gradle: This file specifies the dependencies required for the project. It includes dependencies for Room, ViewModel, and Kotlin.

Summary

This code showcases how to integrate Jetpack Compose with Room to manage data in an Android application. The ViewModel handles data access and updates the UI through state flows, ensuring a reactive and data-driven user experience. The code also demonstrates basic functionalities like adding, clearing, and displaying data in a list.


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.employees.collectAsState()

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

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

            Button(onClick = {
                val employees = mutableListOf<Employee>()
                for (i in 1..10){
                    employees.add(
                        Employee(
                            null,UUID.randomUUID().toString()
                        )
                    )
                }
                model.insert(Employee(null,UUID.randomUUID().toString()))
            }) { Text(text = "Add One") }
        }

        Row(verticalAlignment = Alignment.CenterVertically) {
            TextButton(onClick = { model.clear() }) {
                Text(text = "Clear")
            }
            Spacer(modifier = Modifier.width(12.dp))
            Text(
                text = if (textState.value.trim().isEmpty()){"Employees"}
                else {"Employees start with (${textState.value.trim()})"} +
                        " ${list.value.size}",
                fontWeight = FontWeight.Bold
            )
        }

        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 ORDER BY id DESC")
    fun allEmployees():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 _Employees = MutableStateFlow(emptyList<Employee>())
    var employees: StateFlow<List<Employee>> = _Employees

    init {
        loadEmployees()
    }

    fun loadEmployees() =  effect {
        _Employees.value = db.employeeDao().allEmployees()
        Log.d("xapp", "Loading employees")
    }

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

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

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

    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