
plugins {
id("org.jetbrains.kotlin.kapt")
}
dependencies {
val room_version = "2.5.0"
implementation("androidx.room:room-runtime:$room_version")
// To use Kotlin annotation processing tool (kapt)
kapt("androidx.room:room-compiler:$room_version")
}
엔티티: ToDoEntity
데이터 접근 객체: ToDoDao
룸 데이터베이스: AppDatabase
[app] ➡️ [kotlin+java] ➡️ [com.example.todolistapp] ➡️ 마우스 우클릭 ➡️ [New] ➡️ [Package] ➡️ db 패키지 생성
[db] ➡️ [New] ➡️ [Kotlin class/File] ➡️ ToDoEntity 생성
ToDoEntity에 다음 코드 작성
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity // 구성요소 알려주는 어노테이션 필수
data class ToDoEntity (
// 기본키 => 각 정보를 식별하는 값
// autoGenerate = true => id가 자동으로 1씩 증가되며 저장
@PrimaryKey(autoGenerate = true) var id : Int? = null,
@ColumnInfo(name = "title") val title : String,
@ColumnInfo(name = "importance") val importance : Int
)
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
@Dao
interface ToDoDao {
// 생성한 ToDoEntity에서 모든 데이터를 불러오는 쿼리 함수
@Query("select * from ToDoEntity")
fun getAll() : List<ToDoEntity>
//ToDoEntity 객체를 테이블에 삽입
@Insert
fun insertTodo(todo: ToDoEntity)
// 특정 ToDoEntity 객체를 테이블에서 삭제
@Delete
fun deleteTodo(todo: ToDoEntity)
}
데이터베이스 클래스 만족 조건
1. @Database 어노테이션에서 데이터베이스와 관련된 모든 Entity를 나열
2. RoomDatabase를 상속하는 추상 클래스
3. DAO를 반환하고 인수가 존재하지 않는 추상 함수가 있음
[db] ➡️ 아무스 우클릭 ➡️ [New] ➡️ [Kotlin class/File] ➡️ AppDatabase 생성
AppDatabase에 다음 코드 작성
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = arrayOf(ToDoEntity::class), version = 1) // 조건 1
abstract class AppDatabase : RoomDatabase() { // 조건 2
abstract fun getTodoDao() : ToDoDao // 조건 3
companion object {
val databaseName = "db_todo" // 데이터베이스 명
var appDatabase : AppDatabase? = null
fun getInstance(context : Context) : AppDatabase? { // 싱글턴 패턴 함수 구현
if (appDatabase == null) {
// appDatabase가 null일 때 객체 생성
appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, databaseName).build()
}
// null이 아니면 기존 객체 반환
return appDatabase
}
}
}
💡 싱글턴 패턴
- 데이터베이스를 쓸 때마다 객체를 생성하기보다는 한 객체를 여러 클래스에서 글로벌하게 쓰는게 더 효율적
👉 싱글턴 패턴
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.todolistapp.databinding.ActivityAddTodoBinding
import com.example.todolistapp.db.AppDatabase
import com.example.todolistapp.db.ToDoDao
import com.example.todolistapp.db.ToDoEntity
class AddTodoActivity : AppCompatActivity() {
lateinit var binding: ActivityAddTodoBinding
lateinit var db : AppDatabase
lateinit var todoDao: ToDoDao // ToDoDao인터페이스 => insert, delete, query 기능 제공
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddTodoBinding.inflate(layoutInflater)
setContentView(binding.root)
db = AppDatabase.getInstance(this)!!
todoDao = db.getTodoDao()
binding.btnCompletion.setOnClickListener {
insertTodo()
}
}
private fun insertTodo() {
val todoTitle = binding.editTitle.text.toString() // 할 일의 제목
var todoImportance = binding.radioGroup.checkedRadioButtonId // 할 일의 중요도
when(todoImportance) {
R.id.btn_high -> {
todoImportance = 1
}
R.id.btn_middle -> {
todoImportance = 2
}
R.id.btn_low -> {
todoImportance = 3
}
else -> {
todoImportance = -1
}
}
// 중요도가 선택되지 않았거나, 제목이 작성되지 않았는지 체크
if (todoImportance == -1 || todoTitle.isBlank()) {
Toast.makeText(this, "모든 항목을 채워주세요.", Toast.LENGTH_SHORT).show()
} else {
// 백그라운드 스레드 실행
Thread {
todoDao.insertTodo(ToDoEntity(null, todoTitle, todoImportance))
runOnUiThread {
Toast.makeText(this, "추가되었습니다.", Toast.LENGTH_SHORT) .show()
finish()
}
}
}
}
}
🚨 데이터베이스 관련 작업 시 주의점
- 작업량이 큰 데이터베이스 관련 작업은 반드시 백그라운드 스레드에서 진행
- 이를 실무에서는 Coroutines, RxJava, RxKotlin과 같은 라이브러리 사용(비동기 처리)

선형: LinearLayoutManager
격자형: GridLayoutManager
지그재그 격자형: StaggeredGridLayoutManager
item_todo.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_importance"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="20dp"
android:background="@color/yellow"
android:gravity="center"
android:text="2"
android:textColor="@color/white"
android:textSize="19sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="20dp"
android:text="GO TO GYM"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_importance"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.todolistapp.databinding.ActivityAddTodoBinding
import com.example.todolistapp.databinding.ItemTodoBinding
import com.example.todolistapp.db.ToDoEntity
class ToDoRecyclerViewAdapter (private val todoList: ArrayList<ToDoEntity>) : // 어댑터 객체를 생성할 때 todoList를 인수로 받아줌
// RecyclerView.Adapter<MyViewHolder클래스>를 상속 => 뷰홀더 패턴
RecyclerView.Adapter<ToDoRecyclerViewAdapter.MyViewHolder>() {
// MyViewHolder 클래스 생성
inner class MyViewHolder(binding : ItemTodoBinding) : RecyclerView.ViewHolder(binding.root) {
val tv_importance = binding.tvImportance
val tv_title = binding.tvTitle
val root = binding.root
}
// MyViewHolder 객체 생성
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
// item_todo.xml 뷰 바인딩 객체 생성
val binding: ItemTodoBinding = ItemTodoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
// 받아온 데이터를 MyViewHolder객체에 어떻게 넣어줄지 결정
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val todoData = todoList[position]
// 중요도에 따라 색상 변경
when (todoData.importance) {
1 -> {
holder.tv_importance.setBackgroundResource(R.color.red)
}
2 -> {
holder.tv_importance.setBackgroundResource(R.color.yellow)
}
3 -> {
holder.tv_importance.setBackgroundResource(R.color.green)
}
}
// 중요도에 따라 중요도 텍스트 변경
holder.tv_importance.text = todoData.importance.toString()
// 할 일의 제목 변경
holder.tv_title.text = todoData.title
}
// 데이터가 몇 개인지 반환
override fun getItemCount(): Int {
return todoList.size
}
}
💡 뷰홀더 패턴
- 각 뷰 객체를 뷰 홀더에 보관하여 반복적인 메서드 호출을 줄여 속도를 개선하는 패턴
- 뷰 홀더 클래스를 만들고, 안에 변수를 선언하면 뷰에 즉시 액세스가 가능해짐
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.todolistapp.databinding.ActivityMainBinding
import com.example.todolistapp.db.AppDatabase
import com.example.todolistapp.db.ToDoDao
import com.example.todolistapp.db.ToDoEntity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var db : AppDatabase
private lateinit var toDoDao: ToDoDao
private lateinit var todoList: ArrayList<ToDoEntity>
private lateinit var adapter: ToDoRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnAdd.setOnClickListener {
val intent = Intent(this, AddTodoActivity::class.java)
startActivity(intent)
}
// DB 인스턴스를 가져오고 DB작업을 할 수 있는 DAO를 가져옴
db = AppDatabase.getInstance(this)!!
toDoDao = db.getTodoDao()
getAllTodoList()
}
private fun getAllTodoList() {
// 백그라운드에서 실행
Thread {
todoList = ArrayList(toDoDao.getAll())
setRecyclerView()
}.start()
}
private fun setRecyclerView() {
// UI 작업이므로 UI 스레드에서 실행
runOnUiThread {
adapter = ToDoRecyclerViewAdapter(todoList) // 어댑터 객체 할당
// 리사이클러뷰 어댑터로 위에서 만든 어댑터 설정
binding.recyclerView.adapter = adapter
// 레이아웃 매니저 설정
binding.recyclerView.layoutManager = LinearLayoutManager(this)
}
}
// 액티비티가 멈췄다가 다시 시작되었을 때 실행되는 함수(생명주기 관련)
override fun onRestart() {
super.onRestart()
getAllTodoList()
}
}


interface OnItemLongClickListener {
// 길게 클릭되었을 때 실행
fun onLongClick(position: Int)
}
class ToDoRecyclerViewAdapter (private val todoList: ArrayList<ToDoEntity>, private val listener: OnItemLongClickListener) : // OnItemLongClickListener인터페이스 구현체 넘겨주기
RecyclerView.Adapter<ToDoRecyclerViewAdapter.MyViewHolder>() {
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val todoData = todoList[position]
// 중요도에 따라 색상 변경
when (todoData.importance) {
1 -> {
holder.tv_importance.setBackgroundResource(R.color.red)
}
2 -> {
holder.tv_importance.setBackgroundResource(R.color.yellow)
}
3 -> {
holder.tv_importance.setBackgroundResource(R.color.green)
}
}
// 중요도에 따라 중요도 텍스트 변경
holder.tv_importance.text = todoData.importance.toString()
// 할 일의 제목 변경
holder.tv_title.text = todoData.title
// 할 일이 길게 클릭되었을 때 리스너 함수 실행
holder.root.setOnClickListener { // holder.root => 한 아이템뷰의 루트 레이아웃
listener.onLongClick(position)
false
}
}
}
override fun onLongClick(position: Int) {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle("할 일 삭제")
builder.setMessage("정말 삭제하시겠습니다?")
builder.setNegativeButton("취소", null)
builder.setPositiveButton("네",
object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
deleteTodo(position)
}
})
builder.show()
}
private fun deleteTodo(position: Int) {
Thread {
toDoDao.deleteTodo(todoList[position]) // DB에서 삭제
todoList.removeAt(position) // 리스트에서 삭제
runOnUiThread { // UI 관련 작업은 UI 스레드에
adapter.notifyDataSetChanged() // 어댑터에게 데이터가 바뀌었음을 알려줌 => 리사이클러뷰가 그에 맞춰 자동 업데이트
Toast.makeText(this, "삭제되었습니다.", Toast.LENGTH_SHORT).show()
}
}.start()
}
