1. entity
데이터베이스의 테이블 역할 하는 데이터 클래스
2. DAO (data access object)
앱에서 필요한 모든 작업에 필요한 쿼리를 포함하는 인터페이스
인터페이스 위에 꼭 @Dao 붙여야함
@Insert @Fetch @Update @Delete @Query 로 사용자 지정 SQL 문장 작성
3. database
@Database 주석 필수
Room db를 확장함, 엔티티와 설정된 db를 포함하도 db 주요 액세스 포인트 역할을한다
앱수준 빌드그래이들 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")
}
[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" }
// Room
implementation(libs.androidx.room.runtime) //room and lifecycle 종속성 사용
kapt(libs.androidx.room.compiler) //kotlin 주석 프로세스는 이 명령줄로 불러옴
implementation(libs.androidx.room.ktx) //Room의 공동 루틴 지원 확장을 불러옴
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 = "",
)
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>
}
database class에서 작성 해야 하는 것
- 사용할 버전과 엔티티를 정의
@Database(entities = [EmployeeEntity::class], version = 1)
만약 db가 없데이트(예. 테이블에 있는 속성 변경등) 되면 version을 수정해야됨 그리고 데이터 이동을 하는데 프로젝트에 새 속성이 있어야함 그 속성이 없으면 변경, 생성을 매번 해줘야 하기 때문
단, 데모 버전에선 변경될 때 마다 데이터 삭제하고 처음부터 다시 시작할 것 실제 db에서 그러면 안됨 (주의)- db를 dao에 연결
abstract fun employeeDao() : EmployeeDao
- 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
}
}
}
}
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>
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)
}
}
}
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
// }
// }
}
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>
//다이아 로그에 내가 만든 화면 띄우기
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()