KUIT-앱 개발 프로젝트 동아리

7주차 실습(1)-roomDB

농농씨 2023. 11. 10. 19:24

1. 프로젝트 만들고

2. 그래들 dependendcies에 room

// RoomDB
    implementation("androidx.room:room-runtime:2.6.0")
    annotationProcessor("androidx.room:room-compiler-processing:2.6.0")
    //    kapt("androidx.room:room-compiler:$room_version")

맨마지막줄은 왜 잘 안되는줄 모르겠음

 

3. 똑같이 그래들 android에 뷰바인딩 설정해주고

4. activity_main.xml에 아무 리사이클러 뷰 선언하고 아이디 달아줌 id는 rv_home

5. MainActivity에 뷰바인딩 하기

package com.iyr.a7thweekpractice

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.iyr.a7thweekpractice.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    lateinit var binding : ActivityMainBinding

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

        binding.rvHome.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

    }
}

 

6. 리사이클러뷰 어댑터 만들어주기

package com.iyr.a7thweekpractice

import androidx.recyclerview.widget.RecyclerView

class JustAdapter(var items: List<String>): RecyclerView.Adapter<JustAdapter.ViewHolder>() {
    inner class ViewHolder(val binding : )
}

inner class ViewHolder 만들어주고 아이템레이아웃만들러 가기

 

7. item_home.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="wrap_content">

    <TextView
        android:id="@+id/tv_item_home"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:text="item"
        android:textSize="50sp"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

 

8. 다시 어댑터에서 바인딩부터 해주기

 

8-1. 아이템 레이아웃 바인딩

inner class ViewHolder(val binding : ItemHomeBinding) : RecyclerView.ViewHolder(binding.root) {
        
    }

8-2. 3가지 메서드 override

package com.iyr.a7thweekpractice

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.iyr.a7thweekpractice.databinding.ItemHomeBinding

class JustAdapter(var items: List<String>): RecyclerView.Adapter<JustAdapter.ViewHolder>() {
    inner class ViewHolder(val binding : ItemHomeBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item : String) {
            binding.tvItemHome.text = item
        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun getItemCount(): Int {
        return items.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items[position])
    }


}

여기까지는 항상 쓰는 코드.

getItemCount에서 item 크기 반환해주고,

 

onCreateViewHolder에서 아이템레이아웃 바인딩한 다음

뷰홀더에 바인딩한거 넣어서 반환해주고,

 

(ViewHolder에 bind 함수 선언해서(바인딩한 텍스트뷰를 전달받은 item 값으로 세팅하도록))

onBindViewHolder에서 bind 함수 호출하면서 items의 position 값 전달.

 

9. MainActivity에서 데이터 리스트 생성하고 어댑터 연결

package com.iyr.a7thweekpractice

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.iyr.a7thweekpractice.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    lateinit var binding : ActivityMainBinding
    val dataList : ArrayList<String> = arrayListOf<String>("item0", "item1", "item2", "item3", "item4", "item5", "item6", "item7")

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

        binding.rvHome.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        binding.rvHome.adapter = JustAdapter(dataList)

    }
}

리사이클러뷰 자체의 어댑터에 내가 만든 어댑터에 데이터리스트 전달한 것을 할당해주기

 

리사이클러뷰 출력 화면.

여기까진 복습.

 

 

이제 리사이클러뷰의 아이템을 데이터베이스에 저장해서 가져오는 것을 해볼 것이다.

 

10. 소스파일로 entity(테이블) 추가. 이름은 MyStringEntity

10-1. @Entity 라는 거 알려주고(class 밖에 추가) 이름은 MyStringTable

@Entity(tableName = "MyStringTable")

10-2. data class로 바꿔주고

10-3. 변수는 string으로 하나만(소괄호 안에)

10-4. (중괄호 안에)데이터베이스가 가져야 하는 키 선언

@PrimaryKey(autoGenerate = true) // 아이디가 자동으로 0,1,2,3,4로 작성될 수 있도록 하는 키

 

초기값은 0

var id : Int = 0

 

 

11. entity(DB)에 접근할 수 있는 DAO(Data Access Object) 만들어주기

코틀린파일 새로 만듦. 이름은 MyStringDAO

11-1. entity랑 똑같이 class 밖에 @Dao 라고 명시

11-2. class 가 아닌 interface로 바꾸기

11-3. @Insert

11-4. (함수선언)이 DAO를 통해 entity로 접근해서 (myString이라는) 객체 추가해줄거임

@Insert
fun addMyString(myString : MyStringEntity)

 

MyStringDao 객체를 가져와서(인터페이스로부터?) 

addMyString 함수를 호출해서

그 인자로 myString 객체를 넣어주면 

테이블에 써진다!

 

11-5. @Update, @Delete도 똑같음

@Update
fun updateMyString(myString : MyStringEntity)

@Delete
fun deleteMyString(myString : MyStringEntity)

 

11-6. 근데 Select는 옵션이 많아서 따로 작성해줘야함

간단하게 전체를 선택하는 query문을 작성해보겠다.

query 문법은 따로 공부해야함

@Query("SELECT * FROM MyStringTable")
fun getAllMyStrings() : List<MyStringEntity>

 

package com.iyr.a7thweekpractice

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update

@Dao
interface MyStringDAO {
    @Insert
    fun addMyString(myString : MyStringEntity)

    @Update
    fun updateMyString(myString : MyStringEntity)

    @Delete
    fun deleteMyString(myString : MyStringEntity)

    @Query("SELECT * FROM MyStringTable")
    fun getAllMyStrings() : List<MyStringEntity>

}

 

 

12. 이 DAO를 담고 있는 데이터베이스 객체를 만들어주자

New 코틀린 파일-이름은 MyStringDatabase, 그리고 RoomDatabase를 상속받아야 함

abstract class MyStringDatabase : RoomDatabase()

12-1. @Database 명시(class 밖에)

12-2. 명시할 게 많음.

먼저, 가지고 있는 테이블이 뭔지, 데이터베이스 버전 몇인지 등등

이 두가지는 필수적으로 명시

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

 

12-3. class -> abstract class로 바꿈

 

12-4. 데이터베이스는 entity에 접근할 수 있는 dao들을 가짐. 그 dao들을 가져오는 abstract 함수 선언.

(가져옴으로써 편집 등을 할 수 있도록)

 

싱글톤 패턴이란?

싱글톤(Singleton) 패턴의 정의는 단순하다. 객체의 인스턴스가 오직 1개만 생성되는 패턴

장점 1. 메모리측면

장점 2. 클래스 간의 데이터 공유가 쉬움

싱글톤 패턴 이용

일단 객체 생성하고

var instance

static 함수로 MyString DB를 가져올수있도록 fun getInstance 선언함(? 이해안됨~)

 

instance 가 null이면

Room이라는 object의 databaseBuilder()라는 static 함수이용해서 데이터 할당(?)

 

package com.iyr.a7thweekpractice

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

@Database(entities = [MyStringEntity::class], version = 1)
abstract class MyStringDatabase : RoomDatabase() {
    abstract fun getMyStringDAO() : MyStringDAO

    companion object {
        var instance : MyStringDatabase? = null

        fun getInstance(context: Context) : MyStringDatabase {
            if(instance == null) {
                instance = Room.databaseBuilder(
                    context,
                    MyStringDatabase::class.java,
                    "mystring-database"
                ).fallbackToDestructiveMigration()
                    // 데이터 베이스 버전 올릴 때마다 데이터베이스 초기화됨
                    .allowMainThreadQueries().build() // databaseBuilder() 뒤에 꼭 build() 붙여야함
                // 원래는 안되지만 일단 스레드 생성 없이 메인스레드에서 데이터에 접근하도록 요청함.
            }

            return instance!!
        }
    }


}

싱글톤 패턴으로 구성된 MyStringDatabase라는 함수(추상클래스?)가 만들어졌음

 

이 추상 클래스는

-RoomDatabase()를 상속하고 있고

abstract class MyStringDatabase : RoomDatabase()

 

-MyStringDAO를 가져올 수 있는 추상 함수도 있다.

abstract fun getMyStringDAO() : MyStringDAO

 

그럼 객체는 언제 생성하냐?

 

객체를 코드에서 직접 생성하는 게 아닌,

MyStringDatabase.getInstance 로 가져올거임

fun getInstance 안에서 Room이라는 object 안에 있는 databaseBuidler()라는 함수가 생성함

 

알아서 dao도, entity, db도 생성해줌

이제 dao, entity, db 구성이 끝났음

 

몇가지 편의를 위한 함수를 추가해주자면

.fallbackToDestructiveMigration()

데이터베이스 버전 올릴때마다 데이터 베이스가 초기화됨

.allowMainThreadQueries().build()

원래 안되는데 메인스레드에서 데이터 접근할수있도록 함.

 

13. 이제 데이터베이스를 사용해보자.

13-1. 우선 MainActivity에서, 데이터를 살짝 바꿔주자

<변경전>

val dataList : ArrayList<MyString> = arrayListOf<MyString>("item0", "item1", "item2", "item3", "item4", "item5", "item6", "item7")

<변경후>

var dataList1 = ArrayList<MyString>() // 초기화 해야함

데이터 따로 안넣어주고 빈 Arraylist로 선언

 

❗️트러블슈팅

변수선언할 땐 꼭 초기화를 해주거나(대입연산자가 있어야한단 뜻)

아니면 lateinit과 같은 초기화 지연을 해줘야한다.

var dataList : ArrayList<MyStringEntity> = arrayListOf<MyStringEntity>()

또는

var dataList = ArrayList<MyStringEntity>() 와 같이 초기화

 

13-2. 데이터베이스 객체 만들기

아까는 item0과 같은 데이터를 넣은 리스트를 선언해서 바로 어댑터에 넣어줬다.

이번에는 그 대신 데이터베이스 객체를 가져와서

데이터베이스 안에 데이터 저장한 다음,

데이터베이스 안에 저장된 데이터를 가져와서~ 그걸 어댑터에 넘겨주도록 하겠다.

(아까는 리스트에 직접 데이터 넣었으면 이번에는 db의 데이터를 이용하겠단 뜻)

 

그러므로 데이터베이스 객체부터 만들어주자

var myStringDatabase : MyStringDatabase? = null

 

13-3. 데이터베이스 객체 가져오기

onCreate 함수에서

myStringDatabase 변수에 MyStringDatabase 객체를 가져올 것이다.

 

MyStringDatabase 객체는 싱글톤 패턴이다.

그리고 MyStringDatabase() 이런 식으로 직접 객체를 만드는 게 아니라

myStringDatabase = MyStringDatabase.getInstance(this)

이런 식으로 그 안에 있는 getInstance라는 함수를 활용해서 객체를 가져오는 것이다.

 

그리고

myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item0"))

myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item1"))

그 안에 있는 getMyStringDAO에 접근을 해서 데이터를 넣을 것이다.(addMyString)

getMyStringDAO()를 이용해서 entity에 MyString 객체를 넣어줄 것이다.

그리고 값 넣어서 초기화해줌(item0, item1)

 

13-4. 데이터 베이스 가져오기

데이터를 넣었으니 가져와보자

 

똑같이 onCreate안에서

myStringDatabase!!.getMyStringDAO().getAllMyStrings()

또 getMyStringDAO에 접근해서

getAllMyStrings 선언해놨던 걸로 모든 데이터를 가져오도록 한다.

 

그리고 그걸 dataList라는 아까 선언한 변수에 저장해준다.
dataList.addAll(myStringDatabase!!.getMyStringDAO().getAllMyStrings())

 

13-5. 데이터타입도 알맞게 바꿔주기

이때 아까 dataList를 그냥 ArrayList<String>으로 선언하는 게 아니라

String을 MyStringEntity로 바꿔줘야 한다.

 

어댑터도 바꿔준다

var items: List<String> -> <MyStringEntity>

fun bind(item : String) -> (item : MyStringEntity)

binding.tvItemHome.text = item

->

binding.tvItemHome.text = item.str

 

// MainActivity 전체코드

package com.iyr.a7thweekpractice

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.iyr.a7thweekpractice.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    lateinit var binding : ActivityMainBinding

    var dataList1 = ArrayList<MyStringEntity>() // 초기화 해야함
    var dataList2 : ArrayList<String> = arrayListOf() // null로 선언
    var myStringDatabase : MyStringDatabase? = null // 데이터베이스 객체 만듦

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

        myStringDatabase = MyStringDatabase.getInstance(this)
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item0"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item1"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item2"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item3"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item4"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item5"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item6"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item7"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item8"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item9"))
        myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item10"))

        dataList1.addAll(myStringDatabase!!.getMyStringDAO().getAllMyStrings())


        binding.rvHome.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        binding.rvHome.adapter = JustAdapter(dataList1)

    }
}

 

// JustAdapter 전체 코드
package com.iyr.a7thweekpractice

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.iyr.a7thweekpractice.databinding.ItemHomeBinding

class JustAdapter(var items: List<MyStringEntity>): RecyclerView.Adapter<JustAdapter.ViewHolder>() {
    inner class ViewHolder(val binding : ItemHomeBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item : MyStringEntity) {
            binding.tvItemHome.text = item.str
        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun getItemCount(): Int {
        return items.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items[position])
    }


}

 

[정리]

우리가 한 일은

1. 데이터 베이스 객체 가져와서

myStringDatabase = MyStringDatabase.getInstance(this)

2. 데이터 넣고

myStringDatabase!!.getMyStringDAO().addMyString(MyStringEntity("item0"))

3. 그 데이터베이스에서 데이터 모두 가져온걸, dataList라는 변수에 넣은 다음

dataList.addAll(myStringDatabase!!.getMyStringDAO().getAllMyStrings())

4. 그 dataList를 가지고 어댑터 만듦

binding.rvHome.adapter = JustAdapter(dataList)

 

그러면 데이터가 실행할 때마다 누적되면서 새로 추가된다.

데이터 지우고 싶으면 버전을 업데이트 하면 초기화되고 한번만 데이터가 추가된 상태로 실행된다.

 

(내 프로젝트는 오류났는데 왜났는지 몰겟음~