이번에는 Room 을 사용해서 로컬 데이터 베이스에 데이터를 저장하는 간단한 예제를 해보겠습니다.
insert 버튼 클릭시 데이터 베이스에 데이터 저장
getdata 버튼 클릭시 모든 데이터를 가져와서 리사이클러뷰에 표시
delete 버튼 클릭시 모든 데이터 삭제
출처 : 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'
}
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 가 컬럼값으로 들어가게 되고, 결국엔 테이블의 한 행이 생성되게 됩니다.
데이터베이스에 대해서 공부해본적이 있다면 이해하기 쉬울 것 같습니다.
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 입니다.
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 를 확장하는 추상 클래스여야 합니다.
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 에서는 데이터베이스를 생성하고, 상호작용하는 코드를 구현합니다.
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 를 명시적으로 지정해줘야 합니다.
<?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>
리사이클러뷰 아이템 레이아웃입니다.
<?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>
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
}
}
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