Jetpack 라이브러리: Room

Soomin Kim·2023년 3월 3일
0

Android

목록 보기
5/14

Room

SQLite의 Wrapper 처럼 만들어져서, 모든 열을 거치지 않고도 데이터를 쿼리할 수 있는 복잡한 문장을 쉽게 작성할 수 있음.

구성요소

  1. Entity: 데이터베이스의 테이블 역할을 하는 데이터 클래스. 열을 구축하기 위한 변수를 포함함.
    추가된 엔티티와 키워드는 클래스에 주석을 달아야 하고, 프라이머리 키 역할을 하는 변수가 있어야됨. 주석에 프라이머리 키라고 표시.

  2. Dao(Data Access Object): 앱에서 필요한 모든 작업에 필요한 쿼리를 포함하는 인터페이스. @Insert, @Fetch, @Update, @Delete 같은 편리한 방법이 있음. @Query로 사용자 지정 SQL 문장을 작성할 수도 있음. 인터페이스에 꼭 @Dao라고 주석을 달아야 됨.

  3. Database
    이 클래스는 @Database라고 주석을 달았고, Room 데이터베이스를 확장. 엔티티와 함께 설정된 데이터베이스를 포함하고, 데이터베이스에 대한 주요 엑세스 포인트 역할을

Flow: Corountines 클래스의 일부이며, 런타임에서 바뀔 수 있는 값을 가짐.
Flow를 사용하면, 변수나 메소드에서 값을 'collect'하기만 하면 됨. 사용자 인터페이스에서 코드를 업데이트할 필요도 없음.

val updates:Flow<List<User>> = emptyFlow

updates.collect{userList->
    setupUi(userList)
}

Flow를 청취하기 위한 다른 방법

  • collectLatest: 업데이트 되면 이전의 값은 폐기
  • collectIndexed: 구성 요소의 값과 연관된 인덱스를 가져옴
  • combine: 변화가 생기면 Flow를 변환하고 값을 반환함.

Entity

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 = ""
)

EmployeeDao

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>

}

Employee Database

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

            }
        }




    }


}

Employee App

package com.example.roomdemo

import android.app.Application

// Dao에 접근하기 위한 클래스
class EmployeeApp: Application() {
    // 모든 애플리케이션 클래스는 Manifest에 정의되어 있어야 함.
    // LAZY: 필요할때만 변수 전달
    val db by lazy{
        EmployeeDatabase.getInstance(this)
    }
}

ItemAdapter

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)
        }
    }
}

MainActivity

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()

    }
}
profile
개발자지망생

0개의 댓글