<MVVM 패턴 공부> Room

LeeEunJae·2022년 9월 20일
1

Study Kotlin

목록 보기
18/20

이번에는 Room 을 사용해서 로컬 데이터 베이스에 데이터를 저장하는 간단한 예제를 해보겠습니다.

insert 버튼 클릭시 데이터 베이스에 데이터 저장
getdata 버튼 클릭시 모든 데이터를 가져와서 리사이클러뷰에 표시
delete 버튼 클릭시 모든 데이터 삭제

📌 실행 화면

📌 Room?


출처 : https://developer.android.com/training/data-storage/room?hl=ko
Room 은 안드로이드 SQLite 를 더욱 편하게 관리하기 위해서 사용하는 라이브러리 입니다.
다음과 같은 이점이 존재하므로 SQLite 를 직접 사용하기 보다는 Room 을 사용하는 것을 공식적으로 권장하고 있습니다.

SQL 쿼리의 컴파일 시간 확인
반복적이고 오류가 발생하기 쉬운 상용구 코드를 최소화하는 편의 주석
간소화된 데이터베이스 이전 경로

Room 구성요소

데이터베이스
데이터 항목(Entity)
데이터 액세스 객체(DAO)

자세한 내용은 예제를 진행하면서 알아보도록 하겠습니다.

의존성 추가

이 예제를 진행하기 위해서 gradle 파일에 의존성을 추가해줍니다.

def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
// To use Kotlin annotation processing tool (kapt)
kapt("androidx.room:room-compiler:$room_version")
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' // viewModelScope
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") // coroutine
plugins {
    ...
    
    id 'kotlin-kapt'
}

Entities

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

@Entity(tableName = "text_table")
data class TextEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Int,
    @ColumnInfo(name = "text")
    var text: String
)
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "word_table")
data class WordEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Int,
    @ColumnInfo(name = "text")
    var text: String
)

Entity는 데이터베이스에 들어갈 데이터 항목을 의미합니다.
테이블 이름을 각각 text_table , word_table 이라고 설정했습니다.

TextEntity 인스턴스를 생성해서 db 에 넣어주면, text_table 이라는 테이블에
textEntity 의 파라미터인 id 와 text 가 컬럼값으로 들어가게 되고, 결국엔 테이블의 한 행이 생성되게 됩니다.

데이터베이스에 대해서 공부해본적이 있다면 이해하기 쉬울 것 같습니다.

DAO(Data Access Object)

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.dldmswo1209.room.entity.TextEntity

@Dao
interface TextDao {

    @Query("SELECT * FROM text_table")
    fun getAllData() : List<TextEntity>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(text: TextEntity)

    @Query("DELETE FROM text_table")
    fun deleteAllData()

}

TextDao는 text_table 테이블과 상호작용 하기 위한 Dao 입니다.

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.dldmswo1209.room.entity.TextEntity
import com.dldmswo1209.room.entity.WordEntity

@Dao
interface WordDao {

    @Query("SELECT * FROM word_table")
    fun getAllData() : List<WordEntity>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(text: WordEntity)

    @Query("DELETE FROM word_table")
    fun deleteAllData()

}

WordDao 는 word_table 과 상호작용 하기 위한 Dao 입니다.

Database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.dldmswo1209.room.dao.TextDao
import com.dldmswo1209.room.dao.WordDao
import com.dldmswo1209.room.entity.TextEntity
import com.dldmswo1209.room.entity.WordEntity

@Database(entities = [TextEntity::class, WordEntity::class], version = 2)
abstract class TextDatabase : RoomDatabase() {
    abstract fun textDao(): TextDao
    abstract fun wordDao(): WordDao

    companion object{
        @Volatile
        private var INSTANCE: TextDatabase? = null

        fun getDatabase(
            context: Context
        ) : TextDatabase {
            return INSTANCE ?: synchronized(this){
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TextDatabase::class.java,
                    "text_database"
                )
                    .fallbackToDestructiveMigration()
                    .build()

                INSTANCE = instance
                instance
            }
        }
    }

}

데이터 베이스를 생성하기 위해 필요한 클래스 입니다.
이 클래스는 RoomDatabase 를 확장하는 추상 클래스여야 합니다.

Repository

import android.content.Context
import com.dldmswo1209.room.db.TextDatabase
import com.dldmswo1209.room.entity.TextEntity
import com.dldmswo1209.room.entity.WordEntity

class Repository(context: Context) {

    val db = TextDatabase.getDatabase(context)

    fun getTextList() = db.textDao().getAllData()

    fun getWordList() = db.wordDao().getAllData()

    fun insertTextData(text: String) = db.textDao().insert(TextEntity(0, text))

    fun insertWordData(text: String) = db.wordDao().insert(WordEntity(0, text))

    fun deleteTextData() = db.textDao().deleteAllData()

    fun deleteWordData() = db.wordDao().deleteAllData()
}

Repository 에서는 데이터베이스를 생성하고, 상호작용하는 코드를 구현합니다.

ViewModel

import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import com.dldmswo1209.room.db.TextDatabase
import com.dldmswo1209.room.entity.TextEntity
import com.dldmswo1209.room.entity.WordEntity
import com.dldmswo1209.room.repository.Repository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


class MainViewModel(application: Application): AndroidViewModel(application) {
    val context = getApplication<Application>().applicationContext
    val repository = Repository(context)

    private var _textList = MutableLiveData<List<TextEntity>>()
    val textList : LiveData<List<TextEntity>>
        get() = _textList

    private var _wordList = MutableLiveData<List<WordEntity>>()
    val wordList : LiveData<List<WordEntity>>
        get() = _wordList

    fun getData() = viewModelScope.launch(Dispatchers.IO) {
        _textList.postValue(repository.getTextList())
        _wordList.postValue(repository.getWordList())
    }

    fun insertData(text: String) = viewModelScope.launch(Dispatchers.IO) {
        repository.insertTextData(text)
        repository.insertWordData(text)
    }

    fun removeData() = viewModelScope.launch(Dispatchers.IO) {
        repository.deleteTextData()
        repository.deleteWordData()
    }

}

ViewModel 에서는 Repository 를 생성하고, Repository 에 데이터를 요청 합니다.

이 코드에서 주목해야 할 점은 viewModelScope.launch(Dispatchers.IO) 입니다.
Dispatchers.IO 를 사용하지 않는다면, 아래와 같은 오류가 발생해서 앱이 중단됩니다.

오류의 내용을 보면 "메인스레드에서는 데이터베이스에 접근할 수 없다" 라고 나와 있습니다.
viewModelScope 의 기본 Dispatcher 는 Dispatcher.Main 입니다.
그러므로 Dispatcher.IO 를 명시적으로 지정해줘야 합니다.

activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <EditText
            android:id="@+id/textInputArea"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/insert"
            android:text="insert"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/getData"
            android:text="getData"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/delete"
            android:text="delete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rcv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

    </androidx.appcompat.widget.LinearLayoutCompat>

</androidx.constraintlayout.widget.ConstraintLayout>

text_row_item

리사이클러뷰 아이템 레이아웃입니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp">

    <TextView
        android:id="@+id/textView"
        android:text="text"
        android:textSize="50sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</androidx.appcompat.widget.LinearLayoutCompat>

Adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.dldmswo1209.room.databinding.TextRowItemBinding
import com.dldmswo1209.room.entity.TextEntity

class CustomAdapter(private val dataSet: List<TextEntity>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

    inner class ViewHolder(val binding: TextRowItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(textEntity: TextEntity){
            binding.textView.text = textEntity.text
        }
    }

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

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

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

MainActivity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.dldmswo1209.room.adapter.CustomAdapter
import com.dldmswo1209.room.databinding.ActivityMainBinding
import com.dldmswo1209.room.db.TextDatabase
import com.dldmswo1209.room.entity.TextEntity
import com.dldmswo1209.room.entity.WordEntity
import com.dldmswo1209.room.viewModel.MainViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        val db = TextDatabase.getDatabase(this)
        viewModel = ViewModelProvider(this)[MainViewModel::class.java]
        viewModel.getData()


        binding.insert.setOnClickListener {
            viewModel.insertData(binding.textInputArea.text.toString())
            binding.textInputArea.text.clear()
        }

        binding.getData.setOnClickListener {
            viewModel.getData()
        }

        binding.delete.setOnClickListener {
            viewModel.removeData()
        }

        viewModel.textList.observe(this, Observer {
            val customAdapter = CustomAdapter(it)
            binding.rcv.adapter = customAdapter
        })


    }

}

📌 출처 및 참고자료

https://www.inflearn.com/course/%EC%B9%9C%EC%A0%88%ED%95%9C-jetpack-1
https://developer.android.com/training/data-storage/room?hl=ko

profile
매일 조금씩이라도 성장하자

0개의 댓글