불순분자 Kotlin [18] - RoomDatabase

불순분자들·2022년 8월 17일
1

List App 만들기

목록 보기
18/18
post-thumbnail

RoomDatabase

룸 데이터베이스를 사용하기 위해서는 앱 모듈에 의존성( dependencies )을 추가해줘야 한다.

// Room Database
    implementation 'androidx.room:room-runtime:2.4.2'
    kapt 'androidx.room:room-compiler:2.4.2'

추가로 plugins에도 하나 추가해줘야한다.

id 'kotlin-kapt'

위와 같이 작성해주면 룸 데이터베이스를 사용할 수 있다.

ListInfo 수정

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
class ListInfo {
    var listContent: String = "" // 메모 내용
    var listDate: String = "" // 메모 일자

    @PrimaryKey(autoGenerate = true)
    var id: Int = 0 // DB에 정보가 들어올 때마다 0,1,2,3,,, 증가함.
}

모델 클래스인 ListInfo를 어노테이션을 붙여서 수정하고 @PrimaryKey로 주식별자를 달아주었다.

database DAO -> Data Access Object 생성

import androidx.room.*
import com.android.listapp.model.ListInfo

// Dao -> Data Access Object
@Dao
interface ListDao {

    // database table에 삽입( Create )
    @Insert
    fun insertListData(listInfo : ListInfo)

    // database table에 수정( Update )
    @Update
    fun updateListDate(listInfo : ListInfo)

    // database table에 삭제( Delete )
    @Delete
    fun deleteListDate(listInfo : ListInfo)

    // database table의 전체 데이터를 조회( Read )
    @Query("SELECT * FROM ListInfo ORDER BY listDate")
    fun getAllReadData(): List<ListInfo>
}

DAO는 데이터 엑세스 오브젝트란 의미이다.
인터페이스는 클래스에서 기능들을 사용하기 위해 캡슐화 하는 것을 뜻한다.
그 캡슐화로 위 코드를 보면 인터페이스에서 데이터베이스의 CRUD를 한 것을 알 수 있다.
조회, R -> Read는 @Query로 요청을 했는데 쿼리는 데이터베이스에서 원하는 정보를 가져오는 코드를 작성할 때 쓰인다한다.
해석하면 모든( * )장소에서 ListInfo를 조회하는데 그것을 listDate순으로 정렬한다는 뜻이다.

ListDatabase 생성

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.android.listapp.model.ListInfo

@Database(entities = [ListInfo::class], version = 1)
abstract class ListDatabase : RoomDatabase() {
    abstract fun listDao(): ListDao

    companion object {
        private var instance: ListDatabase? = null
        @Synchronized // -> 동시에 호출되는 가능성을 방지하기 위해서 사용하고, 순서대로 규칙을 정해 교통정리되는 느낌.
        fun getInstance(context: Context) : ListDatabase? {
            if (instance == null) {
                synchronized(ListDatabase::class) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        ListDatabase::class.java,
                        "list-database"
                    ).build()
                }
            }
            return instance
        }
    }
}
@Database(entities = [ListInfo::class], version = 1)
abstract class ListDatabase : RoomDatabase() {
    abstract fun listDao(): ListDao

@Database 어노테이션을 붙여 데이터베이스임을 암시하고, entities라고 복수형인 이유는 여러 개의 테이블을 넣을 수 있기 때문이다.
abstract를 붙여 추상클래스를 만들고, ListDatabase 클래스는 RoomDatabase()를 상속받는다.
그 후, 만들어두었던 ListDao 인터페이스를 listDao()이름으로 함수를 만들어 둔다.

companion object {
        private var instance: ListDatabase? = null
        @Synchronized // -> 동시에 호출되는 가능성을 방지하기 위해서 사용하고, 순서대로 규칙을 정해 교통정리되는 느낌.
        fun getInstance(context: Context) : ListDatabase? {
            if (instance == null) {
                synchronized(ListDatabase::class) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        ListDatabase::class.java,
                        "list-database"
                    ).build()
                }
            }
            return instance
        }
    }

Companion object는 객체로 자바의 static하고 비슷하다고 볼 수 있다.
메모리에 저장하여 어디서든 인스턴스를 생성하지 않고 사용할 수 있다는 점은 비슷하지만 다른 점으로는 Companion object는 클래스명.Companion으로 사용하여 엄마 강아지의 새끼강아지 느낌이다.

CoroutineScope( 비동기 ) 의존성 추가

// live data
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'

코루틴 스코프를 통해 비동기 통신을 하기 위한 의존성을 앱모듈에 추가해주어야한다.

ListMainActivity에서 RoomDatabase 코드 추가

import android.content.DialogInterface
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.room.RoomDatabase
import com.android.listapp.adapter.TodoAdapter
import com.android.listapp.database.ListDatabase
import com.android.listapp.databinding.ActivityListMainBinding
import com.android.listapp.databinding.DialogEditBinding
import com.android.listapp.model.ListInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList

class ListMainAcitvity : AppCompatActivity() {
    private lateinit var binding: ActivityListMainBinding
    private lateinit var listAdapter: TodoAdapter
    private lateinit var roomDatabase: ListDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityListMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 어댑터 인스턴스 생성
        listAdapter = TodoAdapter()

        // 리사이클러뷰에 어댑터 생성
        binding.rvListLife.adapter = listAdapter

        // 룸 데이터베이스 초기화
        roomDatabase = ListDatabase.getInstance(applicationContext)!!

        // 전체 데이터 로드( 비동기 ) -> 비순서적으로 데이터를 가져오는 방법
        CoroutineScope(Dispatchers.IO).launch {
            val lstTodo = roomDatabase.listDao().getAllReadData() as ArrayList<ListInfo>
            for (todoItem in lstTodo) {
                listAdapter.addListItem(todoItem)
            }
            // UI Thread에서 처리
            runOnUiThread{
                listAdapter.notifyDataSetChanged()

            }
        }

        // 작성하기 버튼 클릭
        binding.btnWrite.setOnClickListener {
            val bindingDialog = DialogEditBinding.inflate(LayoutInflater.from(binding.root.context), binding.root, false)

            AlertDialog.Builder(this)
                .setTitle("오늘의 할 일은 ?")
                .setView(bindingDialog.root)
                .setPositiveButton("작성", DialogInterface.OnClickListener { dialog, i ->
                    // 작성 버튼 이벤트 처리
                    val todoItem = ListInfo()
                    todoItem.listContent = bindingDialog.etMemo.text.toString()
                    // pattern은 이러한 형식을 유지하겠다는 구문이고, format(Date()) 메서드가 현재 시간을 가져와준다.
                    todoItem.listDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
                    listAdapter.addListItem(todoItem) // 어댑터의 전역변수의 arraylist 쪽에 아이템 추가하기위한 메소드 호출
                    CoroutineScope(Dispatchers.IO).launch {
                        roomDatabase.listDao().insertListData(todoItem)
                        runOnUiThread {
                            listAdapter.notifyDataSetChanged() // 리스트 새로고침 -> 어댑터가 한 사이클 돌게 됨. 고로 아이템이 표출됨.
                        }
                    }
                })
                .setNegativeButton("취소", DialogInterface.OnClickListener { dialog, i ->
                    // 취소 버튼 이벤트 처리
                })
                .show()
        }
    }
}
val todoItem = ListInfo()
                    todoItem.listContent = bindingDialog.etMemo.text.toString()
                    // pattern은 이러한 형식을 유지하겠다는 구문이고, format(Date()) 메서드가 현재 시간을 가져와준다.
                    todoItem.listDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
                    listAdapter.addListItem(todoItem) // 어댑터의 전역변수의 arraylist 쪽에 아이템 추가하기위한 메소드 호출
                    CoroutineScope(Dispatchers.IO).launch {
                        roomDatabase.listDao().insertListData(todoItem)
                        runOnUiThread {
                            listAdapter.notifyDataSetChanged() // 리스트 새로고침 -> 어댑터가 한 사이클 돌게 됨. 고로 아이템이 표출됨.
                        }
                    }
                })

위 부분을 보면 ListInfo의 정보를 todoItem의 이름으로 저장하고, todoItem의 listContent는 커스텀한 다이얼로그의 텍스트를 가져온다.
todoItem의 listDate는 SimpleDateFormat을 이용해 위 패턴을 유지하겠다는 뜻이고, format(Date())가 현재 시간을 가져와준다.
그 후, listAdapter에 item을 뿌려주고 그것을 CoroutineScope를 이용해 데이터베이스와 비동기 통신한다.
listAdapter.notifyDataSetChanged()는 새로고침으로 runOnUiThread에서 실행해줘야한다.
이유는 코루틴스코프를 통해서 데이터베이스와 통신을 하는데 새로고침은 UI에 관한 처리이기때문에 에러가 발생한다.

데이터베이스 추가에 따른 어댑터 수정

package com.android.listapp.adapter

import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import androidx.room.RoomDatabase
import com.android.listapp.database.ListDatabase
import com.android.listapp.databinding.DialogEditBinding
import com.android.listapp.databinding.ListItemBinding
import com.android.listapp.model.ListInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList

// ViewHolder는 각 ItemList를 저장하는 객체이다.

class TodoAdapter : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
    private var lstTodo : ArrayList<ListInfo> = ArrayList()
    private lateinit var roomDatabase: ListDatabase

    fun addListItem(listItem: ListInfo) {
        // 배열은 0,1,2,3..으로 쌓이는데 최신 add 정보가 가장 위에 나오게 해야한.다
        // 최신순으로 보이게 하려면 0번째 인덱스의 위치로 add해주면 된다.
        lstTodo.add(0, listItem)
    }

    // TodoViewHolder는 그저 같은 클래스 내부에 작성된 별도의 클래스이므로 inner을 붙임으로써 lstTodo를 사용할 수 있다.
    inner class TodoViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(listItem : ListInfo) {
            // 리스트 뷰 데이터를 UI에 연동
            binding.tvContent.setText(listItem.listContent)
            binding.tvDate.setText(listItem.listDate)

            // 리스트 삭제 버튼 클릭 연동
            binding.btnRemove.setOnClickListener {
                // 쓰레기통 이미지 클릭 시 이벤트 처리

                AlertDialog.Builder(binding.root.context)
                    .setTitle("[경고]")
                    .setMessage("제거하시면 데이터는 복구되지 않습니다.\n정말 제거하시겠습니까?")
                    .setPositiveButton("제거", DialogInterface.OnClickListener { dialog, i ->
                        CoroutineScope(Dispatchers.IO).launch {
                            var innerLst = roomDatabase.listDao().getAllReadData()
                            for (item in innerLst) {
                                if (item.listContent == listItem.listContent && item.listDate == listItem.listDate) {
                                    roomDatabase.listDao().deleteListDate(item)
                                }
                            }

                            // ui remove
                            lstTodo.remove(listItem)
                            (binding.root.context as Activity).runOnUiThread{
                                notifyDataSetChanged()
                                Toast.makeText(binding.root.context, "제거되었습니다.", Toast.LENGTH_SHORT).show()
                            }
                        }
                    })
                    .setNegativeButton("취소", DialogInterface.OnClickListener { dialog, i ->

                    })
                    .show()
            }

            // 리스트 수정
            binding.root.setOnClickListener {
                val bindingDialog = DialogEditBinding.inflate(LayoutInflater.from(binding.root.context), binding.root, false)
                bindingDialog.etMemo.setText(listItem.listContent)

                 androidx.appcompat.app.AlertDialog.Builder(binding.root.context)
                     .setTitle("오늘의 할 일은 ?")
                     .setView(bindingDialog.root)
                     .setPositiveButton("수", DialogInterface.OnClickListener { dialog, i ->
                         CoroutineScope(Dispatchers.IO).launch {
                             val innerLst = roomDatabase.listDao().getAllReadData()
                             for (item in innerLst)
                                 if (item.listContent == listItem.listContent && item.listDate ==listItem.listDate) {
                                     // database modify
                                     listItem.listContent = bindingDialog.etMemo.text.toString()
                                     listItem.listDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
                                     roomDatabase.listDao().updateListDate(item)
                                 }
                         }
                         // ui modify
                         listItem.listContent = bindingDialog.etMemo.text.toString()
                         listItem.listDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
                         // arraylist 수정
                         lstTodo.set(adapterPosition, listItem)
                         (binding.root.context as Activity).runOnUiThread{
                             notifyDataSetChanged()
                         }
                     })
                     .setNegativeButton("취소", DialogInterface.OnClickListener { dialog, i ->
                     // 취소 버튼 이벤트 처리
                     })
                     .show()
            }
        }

    }
    // ViewHoler가 만들어질 때. -> 뷰 홀더가 생성됨( 각 리스트 아이템 1개씩 구성될 때마다 이 오버라이드 메소드가 호출 됨 )
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
        val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        // 룸 데이터베이스 초기화
        roomDatabase = ListDatabase.getInstance(binding.root.context)!!

        return TodoViewHolder(binding)
    }

    // ViewHoler가 결합될 때. -> 뷰 홀더가 바인딩( 결합 )이 이루어질 때 해줘야할 처리들을 구현. position은 index 즉 배열과 비슷함.
    override fun onBindViewHolder(holder: TodoAdapter.TodoViewHolder, position: Int) {
        holder.bind(lstTodo[position])
    }

    // list의 item의 개수를 어댑터에 알려주어야함.
    override fun getItemCount(): Int {
        return lstTodo.size
    }
}

profile
장래희망 : 침대 위 녹아든 치즈

0개의 댓글