SQLite의 Wrapper 처럼 만들어져서, 모든 열을 거치지 않고도 데이터를 쿼리할 수 있는 복잡한 문장을 쉽게 작성할 수 있음.
구성요소
Entity: 데이터베이스의 테이블 역할을 하는 데이터 클래스. 열을 구축하기 위한 변수를 포함함.
추가된 엔티티와 키워드는 클래스에 주석을 달아야 하고, 프라이머리 키 역할을 하는 변수가 있어야됨. 주석에 프라이머리 키라고 표시.
Dao(Data Access Object): 앱에서 필요한 모든 작업에 필요한 쿼리를 포함하는 인터페이스. @Insert, @Fetch, @Update, @Delete 같은 편리한 방법이 있음. @Query로 사용자 지정 SQL 문장을 작성할 수도 있음. 인터페이스에 꼭 @Dao라고 주석을 달아야 됨.
Database
이 클래스는 @Database라고 주석을 달았고, Room 데이터베이스를 확장. 엔티티와 함께 설정된 데이터베이스를 포함하고, 데이터베이스에 대한 주요 엑세스 포인트 역할을
Flow: Corountines 클래스의 일부이며, 런타임에서 바뀔 수 있는 값을 가짐.
Flow를 사용하면, 변수나 메소드에서 값을 'collect'하기만 하면 됨. 사용자 인터페이스에서 코드를 업데이트할 필요도 없음.
val updates:Flow<List<User>> = emptyFlow
updates.collect{userList->
setupUi(userList)
}
Flow를 청취하기 위한 다른 방법
package com.example.roomdemo
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName="employee-table")
data class EmployeeEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String = "",
@ColumnInfo(name = "email-id")
val email: String = ""
)
package com.example.roomdemo
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface EmployeeDao {
// Corountines를 통해 사용할 수 있는 백그라운드 스레드에서 수행해야 함.
@Insert
suspend fun insert(employeeEntity: EmployeeEntity)
@Update
suspend fun update(employeeEntity: EmployeeEntity)
@Delete
suspend fun delete(employeeEntity: EmployeeEntity)
@Query("SELECT * FROM `employee-table`")
fun fetchAllEmployees(): Flow<List<EmployeeEntity>>
// Flow는 변화가 있으면 자동으로 업데이트
// Flow가 suspend나 Coroutine을 대신 처리하기 때문에, Query나 Flow에서는 suspend 쓰면 안됨.
@Query("SELECT * FROM `employee-table` where id=:id")
fun fetchEmployeeById(id:Int): Flow<EmployeeEntity>
}
package com.example.roomdemo
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [EmployeeEntity::class], version = 1)
abstract class EmployeeDatabase: RoomDatabase(){
// Dao - Database 연결
abstract fun employeeDao():EmployeeDao
// Employee Database class에 함수를 추가하는 companion object 정의해야 함.
// getInstance가 반환하는 모든 데이터베이스에 레퍼런스를 유지
// 성능 면에서 비용이 많이 드는 데이터베이스 초기화를 반복적으로 안해도 됨.
// 휘발성 변수의 값은 캐시되지 않으 모든 읽기, 쓰기 작업은 메인 메모리에서 진행. 즉 어떤 스레드에서 변동사항이 생기면 다른 스레드가 관찰할 수 있음.
companion object {
// 휘발성 변수
@Volatile
private var INSTANCE: EmployeeDatabase? = null
// threadsafe가 되고, 호출자는 오버헤드를 피하기 위해 여러 데이터베이스 호출에 대한 결과를 캐시해야 됨.
// 이는 코틀린에서 또 다른 싱글톤 패턴을 가져오는 단순한 싱글톤 패턴의 예
fun getInstance(context: Context):EmployeeDatabase{
// 여러 스레드가 동시에 데이터베이스를 요청할 한번만 초기화할 수 있도록 synchronized 함수 사용
// 한번에 하나의 스레드만 블록에 들어올 수 있음.
synchronized(this){
// INSTANCE의 현재 값을 로컬 변수에 복사해야 함.
// 코틀린은 스마트캐스트 가능 (로컬 변수만)
var instance = INSTANCE
if (instance == null){
instance = Room.databaseBuilder(
context.applicationContext,
EmployeeDatabase::class.java,
"employee_database"
).fallbackToDestructiveMigration()
.build()
// Migration할 객체가 없으면 삭제하고 재구축
INSTANCE = instance
}
return instance
}
}
}
}
package com.example.roomdemo
import android.app.Application
// Dao에 접근하기 위한 클래스
class EmployeeApp: Application() {
// 모든 애플리케이션 클래스는 Manifest에 정의되어 있어야 함.
// LAZY: 필요할때만 변수 전달
val db by lazy{
EmployeeDatabase.getInstance(this)
}
}
package com.example.roomdemo
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.example.roomdemo.databinding.ItemsRowBinding
class ItemAdapter(private val items: ArrayList<EmployeeEntity>,
private val updateListener:(id:Int)->Unit,
private val deleteListener:(id:Int)->Unit
):RecyclerView.Adapter<ItemAdapter.ViewHolder>() {
class ViewHolder(binding: ItemsRowBinding) : RecyclerView.ViewHolder(binding.root){
val llMain = binding.llMain
val tvName = binding.tvName
val tvEmail = binding.tvEmail
val ivEdit = binding.ivEdit
val ivDelete = binding.ivDelete
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemsRowBinding.inflate(LayoutInflater.from(parent.context),parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val context = holder.itemView.context
val item = items[position]
holder.tvName.text = item.name
holder.tvEmail.text = item.email
if(position % 2 == 0){
holder.llMain.setBackgroundColor(ContextCompat.getColor(holder.itemView.context, R.color.colorLightGray))
}else{
holder.llMain.setBackgroundColor(ContextCompat.getColor(holder.itemView.context, R.color.white))
}
// 수정이나 삭제 시 받아온 Listener 실행
holder.ivEdit.setOnClickListener {
// 현재 아이템에서 update 버튼이 눌려졌어~
updateListener.invoke(item.id)
}
holder.ivDelete.setOnClickListener {
deleteListener.invoke(item.id)
}
}
}
package com.example.roomdemo
import android.app.AlertDialog
import android.app.Dialog
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.roomdemo.databinding.ActivityMainBinding
import com.example.roomdemo.databinding.DialogUpdateBinding
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private var binding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
val employeeDao = (application as EmployeeApp).db.employeeDao()
binding?.btnAdd?.setOnClickListener{
// TODO call addrecord with employeeDao
// Application 클래스 만들고 데이터베이스 초기화
addRecord(employeeDao)
}
lifecycleScope.launch{
// Flow를 쓰기때문에 Adapter에 데이터 변경됐다고 알려줄 필요 없음.
employeeDao.fetchAllEmployees().collect{
val list = ArrayList(it)
setUpListOfDataIntoRecyclerView(list, employeeDao)
}
}
}
fun addRecord(employeeDao: EmployeeDao){
val name = binding?.etName?.text.toString()
val email = binding?.etEmailId?.text.toString()
if (name.isNotEmpty() && email.isNotEmpty()){
// 데이터 넣어주는 작업. 백그라운드에서 수행해야되니 corountine 사용
lifecycleScope.launch{
employeeDao.insert(EmployeeEntity(name=name, email=email))
Toast.makeText(applicationContext, "Record saved", Toast.LENGTH_LONG).show()
binding?.etName?.text?.clear()
binding?.etEmailId?.text?.clear()
}
}else{
Toast.makeText(applicationContext, "Name or Email cannot be blank", Toast.LENGTH_LONG).show()
}
}
private fun setUpListOfDataIntoRecyclerView(employeesList:ArrayList<EmployeeEntity>, employeeDao: EmployeeDao){
if(employeesList.isNotEmpty()){
val itemAdapter = ItemAdapter(employeesList,
{
updateId ->
updateRecordDialog(updateId, employeeDao)
},
{
deleteId ->
deleteRecordAlertDialog(deleteId, employeeDao)
}
)
binding?.rvItemsList?.layoutManager = LinearLayoutManager(this)
binding?.rvItemsList?.adapter = itemAdapter
binding?.rvItemsList?.visibility = View.VISIBLE
binding?.tvNoRecordsAvailable?.visibility = View.GONE
}else{
binding?.rvItemsList?.visibility = View.GONE
binding?.tvNoRecordsAvailable?.visibility = View.VISIBLE
}
}
private fun updateRecordDialog(id:Int, employeeDao: EmployeeDao){
val updateDialog = Dialog(this, R.style.Theme_Dialog)
updateDialog.setCancelable(false)
val binding = DialogUpdateBinding.inflate(layoutInflater)
updateDialog.setContentView(binding.root)
lifecycleScope.launch{
employeeDao.fetchEmployeeById(id).collect{
if (it != null){
binding.etUpdateName.setText(it.name)
binding.etUpdateEmailId.setText(it.email)
}
}
}
binding.tvUpdate.setOnClickListener {
val name = binding.etUpdateName.text.toString()
val email = binding.etUpdateEmailId.text.toString()
if (name.isNotEmpty() && email.isNotEmpty()){
lifecycleScope.launch {
employeeDao.update(EmployeeEntity(id, name, email))
Toast.makeText(applicationContext, "Record updated", Toast.LENGTH_LONG).show()
updateDialog.dismiss()
}
}else{
Toast.makeText(applicationContext, "Name or Email cannot be blank", Toast.LENGTH_LONG).show()
}
}
binding.tvCancel.setOnClickListener {
updateDialog.dismiss()
}
updateDialog.show()
}
private fun deleteRecordAlertDialog(id:Int, employeeDao: EmployeeDao){
val builder = AlertDialog.Builder(this)
builder.setTitle("Delete Record")
builder.setIcon(android.R.drawable.ic_dialog_alert)
builder.setPositiveButton("Yes") {dialogInterface, _ ->
lifecycleScope.launch{
employeeDao.delete(EmployeeEntity(id))
Toast.makeText(applicationContext, "Record Deleted", Toast.LENGTH_LONG).show()
}
dialogInterface.dismiss()
}
builder.setNegativeButton("No"){dialogInterface, _ ->
dialogInterface.dismiss()
}
val alertDialog: AlertDialog = builder.create()
alertDialog.setCancelable(false)
alertDialog.show()
}
}