RoomDatabase 사용법 & Dialog에 내가 만든 ui 적용하기(CustomDialog)

소정·2024년 11월 4일
0

Kotlin

목록 보기
31/40

RoomDatabase?

  • 룸은 로컬 데이터베이스에 데이터 저장하는 것
  • SQLite 기능을 최대한 활용하면서도 그 위에 추상화 계층을 제공하여 원활하게 접근 가능하도록함
  • SQLite 기반으로 만들어 진것

룸 구성 3가지

1. entity
데이터베이스의 테이블 역할 하는 데이터 클래스
2. DAO (data access object)
앱에서 필요한 모든 작업에 필요한 쿼리를 포함하는 인터페이스
인터페이스 위에 꼭 @Dao 붙여야함
@Insert @Fetch @Update @Delete @Query 로 사용자 지정 SQL 문장 작성
3. database
@Database 주석 필수
Room db를 확장함, 엔티티와 설정된 db를 포함하도 db 주요 액세스 포인트 역할을한다


사용법

업로드중..

1. bulid.gradle에 kotlin.kapt 추가

  • 앱수준 빌드그래이들 pugin에 kapt 추가 & 디펜던시 하기

  • libs.versions.toml 파일을 사용하는 경우, build.gradle.kts 파일에서 def나 ext로 버전을 정의하는 대신 libs.versions.toml에서 관리하도록 설정

  • pugin에 kapt 추가

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.jetbrains.kotlin.android)
    id("org.jetbrains.kotlin.kapt")
}
  • libs.versions.toml 파일에 버전 추가
[versions]
agp = "8.5.1"
kotlin = "1.9.0"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
material = "1.12.0"
activity = "1.9.3"
constraintlayout = "2.2.0"

# room
#room_version = "2.3.0" //x -> 최신버전으로 쓸것 아님 오류남
room_version = "2.6.1" 

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }

#room
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room_version" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room_version" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room_version" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
  • bulid.gradle에 dependencies 구역에 아래 추가
// Room
    implementation(libs.androidx.room.runtime) //room and lifecycle 종속성 사용
    kapt(libs.androidx.room.compiler) //kotlin 주석 프로세스는 이 명령줄로 불러옴
    implementation(libs.androidx.room.ktx) //Room의 공동 루틴 지원 확장을 불러옴

2. entity 클래스 만들기

  • 데이터베이스의 테이블 역할 하는 데이터 클래스
  • @Entity 어노테이션 필수, 테이블 이름도 지정가능
package com.airpass.romedemo

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

3. DAO (data access object) interface 작성

  • 데이터베이스에 사용 할 쿼리 작성
  • @Dao 어노테이션 필수
  • insert, update, delete는 코루틴 키워드인 suspend를 붙인다
    : 주 스레드에서 사용하면 안되고 백그라운드 스레드한테 시켜야됨

Flow란?

코루틴 클래스의 일부, 런타임에서 바뀔수 있는 값을 가짐
Flow를 사용하면 변수나 메소드에서 값을 collect하기만 하면 됨
자용자 인터페이스에서 코드를 업데이트할 필요가 없다 데이터가 변경되면 알아서 갱신함
-> 리사이클러뷰한테 notifivation 해줄 필요 없어짐

Flow가 청취를 위해 사용하는 방법들
1. collectLatest : 업데이트가 되면 이전의 값은 폐기함
2. collectIndexed : 구성요소의 값과 연관된 인덱스 가져옴
3. combime : 변화가 생기면 Flow를 변환하고 값을 반환

package com.airpass.romedemo

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 {

    @Insert
    suspend fun insert(employeeEntity: EmployeeEntity)

    @Update
    suspend fun update(employeeEntity: EmployeeEntity)

    @Delete
    suspend fun delete(employeeEntity: EmployeeEntity)

    //모든 정보 가져오기
    @Query("SELECT * FROM 'employee-table'")
    fun selectAll():Flow<List<EmployeeEntity>>

    //선택정보만 가져오기
    @Query("SELECT * FROM 'employee-table' WHERE id=:id")
    fun selectByIds(id:Int):Flow<EmployeeEntity>

}

4. database class 작성

  • @Database 주석 필수
  • 데이터베이스 인스턴스 생성 및 dao와 연동

database class에서 작성 해야 하는 것

  1. 사용할 버전과 엔티티를 정의

    @Database(entities = [EmployeeEntity::class], version = 1)

    만약 db가 없데이트(예. 테이블에 있는 속성 변경등) 되면 version을 수정해야됨 그리고 데이터 이동을 하는데 프로젝트에 새 속성이 있어야함 그 속성이 없으면 변경, 생성을 매번 해줘야 하기 때문
    단, 데모 버전에선 변경될 때 마다 데이터 삭제하고 처음부터 다시 시작할 것 실제 db에서 그러면 안됨 (주의)

  2. db를 dao에 연결

    abstract fun employeeDao() : EmployeeDao

  3. db 클래스에 함수를 추가하는 companion object 정의
    : 클래스는 db를 호출하고 인스턴스를 불러와 새로운 인스턴스 생성

    싱글턴으로 만들어서 매번 db가 생성되지않도록 하고 공유하도록 함

package com.airpass.romedemo

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

@Database(entities = [EmployeeEntity::class], version = 1)
abstract class EmployeeDB:RoomDatabase() {

    abstract fun employeeDao() : EmployeeDao

    companion object {

        //getInstance가 반환하는 모든 db에 레퍼런스를 유지하면 초기화를 반복적으로 하지않아도 된다 -> 비용 세이브

        //@Volatile
        //휘발성 변수 만드는 키워드
        //휘발성 변수의 값을 캐시 되지않음,
        //모든 읽기,쓰기 작업이 메인 메모리에서 수행됨 어떤 스레드에서 변동이 생김 다른 스레드가 관찰가능함
        @Volatile
        private var INSTANCE:EmployeeDB?=null

        fun getInstance(context: Context):EmployeeDB {
            //이미 검색된 경우 이전 데이터베이스 반환함
            //이 함수는 'threadsafe'가 되고 오버헤드를 피하기 위해 여러 데이터베이스 호출에 대한 결과를 캐시해야됨
            //싱클톤으로 생성, 한번에 하나의 스레드만 블록에 들어올 수 있음
            synchronized(this) {
                var instance = INSTANCE

                if (instance==null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        EmployeeDB::class.java,
                        "employee_DB"
                    ).fallbackToDestructiveMigration() //이동해야 할 객체 가 없으면 삭제하고 재구축
                        .build()
                    INSTANCE = instance
                }
                return instance
            }
        }

    }

}

5. 애플리케이션 클래스를 만들고 데이터베이스 초기화

  • 앱 시작 단계에서 이 클래스를 호출하여 데이터베이스 초기화 하기 위해 적성
  • 데이터베이스와 같은 리소스를 효율적으로 관리하고 앱 전반에 걸쳐 일관된 접근을 제공하기 위해 사용
  • 모든 어플리케이션 클래스는 매니패스트에 등록 해야됨

애플리케이션 클래스

  • al db by lazy 구문은 Kotlin의 지연 초기화(Lazy Initialization)를 사용하여 데이터베이스 인스턴스를 한 번만 생성, 이 후엔 재사용 -> 메모리 효율성 높이고 불필요한 초기화 방지
package com.airpass.romedemo

import android.app.Application

//애플리케이션 클래스를 만들고 데이터베이스 초기화.
//모든 어플리케이션 클래스는 매니패스트에 등록되어야함
class EmployeeApp:Application() {

    val db by lazy {
        EmployeeDB.getInstance(this)
    }
}

매니패스트

application에 작성한 application 상속받은 클래스 등록
앱에 모든 곳에서 사용해야하기 때문에 'application'에 쓴다

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        
        <!!여기에 삽입!!>
        android:name=".EmployeeApp"
        
        
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.RomeDemo"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

6. recyclerView에 표시할 itemAdater와 로직 작성

  • 비즈니스 로직 분리를 하여 아답터는 데이터의 표시와 사용자 입력 처리하도록 하고 실제 로직은 액티비티에서 하도록 분리한다

6-1) ItemAdater

UI에 데이터 표시만 취급하도록함

invoke() 함수

  • 코틀린에서는 invoke를 안 써도 똑같이 함수처럼 호출할 수 있다
  • invoke는 특별히 쓸 이유가 없으면 생략해도 되지만, 가끔 코드의 의도를 명확히 하거나, 가독성을 높이기 위해 쓰기도 한다
package com.airpass.romedemo

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.airpass.romedemo.databinding.ItemRowBinding

class ItemAdapter(private val items:List<EmployeeEntity>,
    private val updateListener:(id:Int) -> Unit,
    private val deleteListener:(id:Int) -> Unit,
    )
    : RecyclerView.Adapter<ItemAdapter.ViewHolder>() {

    inner class ViewHolder(binding:ItemRowBinding)
        : 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(ItemRowBinding.inflate(LayoutInflater.from(parent.context),parent,false))
    }

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val context = holder.itemView.context
        val item = items[position]

        if (position % 2 ==0) {
            holder.llMain.setBackgroundColor(
                ContextCompat.getColor(context,
                R.color.lightGray))
        } else {
            holder.llMain.setBackgroundColor(
                ContextCompat.getColor(context,
                    R.color.white))
        }
        holder.tvName.text = item.name
        holder.tvEmail.text = item.email

        //클릭
        holder.ivEdit.setOnClickListener {
            updateListener.invoke(item.id) //invoke "함수 호출 버튼" 같은 거
        }
        holder.ivDelete.setOnClickListener {
            deleteListener.invoke(item.id)
        }
    }


}

6-2) MainActivity

  • 실제 비즈니스 로직은 여기에서 처리
  • db에 접근하는 로직은 lifecycleScope를 사용하여 백그라운드에서 실행하도록한다. main thread한테 시키면 app 죽음 mainThread는 ui thread라고 부르기도 하는 ui 담당이기 때문
package com.airpass.romedemo

import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.airpass.romedemo.databinding.ActivityMainBinding
import com.airpass.romedemo.databinding.DialogUpdateBinding
import kotlinx.coroutines.launch
import java.util.ArrayList

class MainActivity : AppCompatActivity() {

    //룸은 로컬 데이터베이스에 데이터 저장하는 것
    //SQLite 기능을 최대한 활용하면서도 그 위에 추상화 계층을 제공하여 원활하게 접근 가능하도록함
    //SQLite 기반으로 만들어 진것

    // 룸 구성 3가지
//     1.entity
//     데이터베이스의 테이블 역할 하는 데이터 클래스
//     2.DAO (data access object)
//     앱에서 필요한 모든 작업에 필요한 쿼리를 포함하는 인터페이스
//     인터페이스 위에 꼭 @Dao 붙여야함
//     @Insert @Fetch @Update @Delete @Query 로 사용자 지정 SQL 문장 작성
//     3.database
//     @Database 주석 필수
//     Room db를 확장함, 엔티티와 설정된 db를 포함하도 db 주요 액세스 포인트 역할을한다

    private var binding : ActivityMainBinding? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding?.root)

        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        //dao에 접근하기 위해서 allpication class를 만들고 매니페니스에 applicaion에 등록해야됨 전체 어플리케이션 앱에서 사용해야하기 때문에
        val emploeeDao = (application as EmployeeApp).db.employeeDao()

        binding?.btnAdd?.setOnClickListener {
            addRecord(emploeeDao )
        }

        lifecycleScope.launch { //백그라운드에 실행되어야하니까
            emploeeDao.selectAll().collect{
                val list = ArrayList(it) //arraylist로 바꾸주기
                setupListOfDataIntoRecyclerView(list,emploeeDao)
            }
        }
    }

    fun addRecord(employeeDao:EmployeeDao) {
        val name= binding?.etName?.text.toString()
        val email= binding?.etEmailId?.text.toString()

        if (name.isNotEmpty() && email.isNotEmpty()) {
            lifecycleScope.launch {
                employeeDao.insert(EmployeeEntity(name=name, email = email))
                Toast.makeText(applicationContext, "저장", Toast.LENGTH_SHORT).show() //코루틴 내부에 있기 떄문에 applicationContext전달 해야됨
                binding?.etName?.text?.clear()
                binding?.etEmailId?.text?.clear()
            }
        } else {
            Toast.makeText(applicationContext, "입력 완료하시오", Toast.LENGTH_SHORT).show()
        }
    }
    
    //업데이트 delete 메소드 => adater한테 알려주기
    private fun updateItme(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.selectByIds(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(this@MainActivity, "업데이트", Toast.LENGTH_SHORT).show()
                    updateDialog.dismiss()
                }
            } else Toast.makeText(this@MainActivity, "빈값", Toast.LENGTH_SHORT).show()
        }

        binding.tvCancel.setOnClickListener {
            updateDialog.dismiss()
        }

        updateDialog.show()

    }
    private fun deleteItme(id: Int, employeeDao: EmployeeDao) {
        AlertDialog.Builder(this).setTitle("삭제")
            .setPositiveButton("YES") { dialogInterface, _ ->
                lifecycleScope.launch {
                    employeeDao.delete(EmployeeEntity(id))
                    Toast.makeText(this@MainActivity, "삭제 완료", Toast.LENGTH_SHORT).show()
                }
                dialogInterface.dismiss()
            }
            .setNegativeButton("No") { dialogInterface, _ ->
                dialogInterface.dismiss()
                Toast.makeText(this@MainActivity, "삭제 취소", Toast.LENGTH_SHORT).show()
            }.setCancelable(false).create().show()
    }

    // 아이템 어댑터한테 리스너들 보내는법
    private fun setupListOfDataIntoRecyclerView(employeeList: ArrayList<EmployeeEntity>,
                                                employeeDao:EmployeeDao) {
        if (employeeList.isNotEmpty()) {
            val itemAdapter = ItemAdapter(employeeList,
                {
                    updateId ->
                    updateItme(updateId, employeeDao)
                },
                {
                        deleteId ->
                    deleteItme(deleteId, employeeDao)
                }
                )
            //리사이클러뷰 가져오기
            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 setupListOfDataIntoRecyclerView(employeeList: ArrayList<EmployeeEntity>,
//                                                employeeDao:EmployeeDao) {
//        if (employeeList.isNotEmpty()) {
//            val itemAdapter = ItemAdapter(employeeList)
//            //리사이클러뷰 가져오기
////            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
//        }
//    }
}


참고) 내가 만든 알럿UI 적용하기

themes.xml 파일에서 style 지정하지않으면 아래 화면과 같이 찐따가 된다

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Base.Theme.RomeDemo" parent="Theme.Material3.DayNight.NoActionBar">
        <!-- Customize your light theme here. -->
        <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
    </style>

    <style name="Theme.RomeDemo" parent="Base.Theme.RomeDemo" />

    <style name="Theme_Dialog" parent="ThemeOverlay.AppCompat.Dialog">
        <item name="android:windowMinWidthMinor">90%</item>
        <item name="android:windowMinWidthMajor">90%</item>
    </style>
</resources>

  • Dialog 두번째 매개변수인 themeResId에 내가 설정한 다이아로그 스타일 적용
//다이아 로그에 내가 만든 화면 띄우기
val updateDialog = Dialog(this, R.style.Theme_Dialog)
        updateDialog.setCancelable(false)
        val binding = DialogUpdateBinding.inflate(layoutInflater)
        updateDialog.setContentView(binding.root)

        lifecycleScope.launch {
            employeeDao.selectByIds(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(this@MainActivity, "업데이트", Toast.LENGTH_SHORT).show()
                    updateDialog.dismiss()
                }
            } else Toast.makeText(this@MainActivity, "빈값", Toast.LENGTH_SHORT).show()
        }

        binding.tvCancel.setOnClickListener {
            updateDialog.dismiss()
        }

        updateDialog.show()

profile
보조기억장치

0개의 댓글