지난번 Realm Database 에 대해서 알아보았고, 이번에는 Room Database 에 대해서 알아보겠습니다.
아래 예제는 구글 코드랩을 따라하며 만들어보았습니다.
아래는 Room Database 흐름의 구조입니다.
Gradle 설정
build.gradle(Module) 파일에 아래 종속성 추가
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.thkim.roomdbtest"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
implementation "androidx.activity:activity-ktx:$rootProject.activityVersion"
// Dependencies for working with Architecture components
// You'll probably have to update the version numbers in build.gradle (Project)
// Room components
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$rootProject.lifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// UI
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation "junit:junit:$rootProject.junitVersion"
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
androidTestImplementation("androidx.test.espresso:espresso-core:$rootProject.espressoVersion", {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestImplementation "androidx.test.ext:junit:$rootProject.androidxJunitVersion"
}
build.gradle(Project) 파일에 아래 종속성 추가
// Top-level build file where you can add configuration options common to all sub-projects/modules.
ext {
activityVersion = '1.1.0'
appCompatVersion = '1.2.0'
constraintLayoutVersion = '2.0.2'
coreTestingVersion = '2.1.0'
coroutines = '1.3.9'
lifecycleVersion = '2.2.0'
materialVersion = '1.2.1'
roomVersion = '2.2.5'
// testing
junitVersion = '4.13.1'
espressoVersion = '3.1.0'
androidxJunitVersion = '1.1.2'
}
buildscript {
ext.kotlin_version = "1.4.10"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
결과물
먼저 결과물입니다.
오른쪽 아래의 버튼을 누르면 데이터를 입력할 수 있는 액티비티가 보이고
저장을 누르면 데이터베이스에 데이터를 저장하게 됩니다.
저장된 데이터는 메인 액티비티에 RecyclerView 를 통해 보여주게 됩니다.
프로젝트 구조
프로젝트 구조는 아래와 같습니다.
Word(Entitiy)
Entity 에 해당하는 Word 클래스를 생성합니다.
Entity 어노테이션에 tableName 을 설정하면 Table 이름이 됩니다.
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
WordDao(DAO)
DAO(Data Access Object) 를 생성합니다.
해당 클래스를 통해 데이터에 접근하게 됩니다.
@Dao
interface WordDao {
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): Flow<List<Word>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
WordRoomDatabase(Database)
Room Database 클래스는 absract 클래스이어야 합니다. 또한 RoomDatabase 를 상속받아야 합니다.
이 클래스의 인스턴스는 싱글톤 패턴을 사용하여야 합니다.
@Database 어노테이션에 entities 에 사용할 데이터베이스를 명시해줍니다.
version 관리를 위해 버전도 명시합니다.
exportSchma 는 schema 의 export 설정을 나타냅니다. 정식 배포 버전에서는 false 를 사용하여 배포하지 않는 것이 좋습니다.
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.build()
INSTANCE = instance
instance
}
}
}
}
WordRepository
Repository 는 필수는 아니지만 코드 분리와 구조에 도움을 줍니다.
이러한 분리는 DAO 와 Network 에 접근을 도와줍니다.
class WordRepository(private val wordDao: WordDao) {
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
WordViewModel
WordViewModel 에서는 Word 데이터를 모델에 요청하는 작업을 하고 받은 데이터를 View 에게 전달합니다.
allWords 변수를 LiveData 로 사용하여 데이터가 변하면 View 에서 변화를 감지하게 됩니다.
class WordViewModel(private val repository: WordRepository) : ViewModel() {
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
RecyclerView
추가되는 데이터 갱신을 위해 RecyclerView 를 사용합니다.
class WordListAdapter : ListAdapter<Word, WordListAdapter.WordViewHolder>(WordsComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
return WordViewHolder.create(parent)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = getItem(position)
holder.bind(current.word)
}
class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val wordItemView: TextView = itemView.findViewById(R.id.textView)
fun bind(text: String?) {
wordItemView.text = text
}
companion object {
fun create(parent: ViewGroup): WordViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(view)
}
}
}
class WordsComparator : DiffUtil.ItemCallback<Word>() {
override fun areItemsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem.word == newItem.word
}
}
}
Singleton Repo & DB
Repository 와 Database 의 인스턴스를 싱글톤으로 만들기 위해 Application 을 시작할 때 단 한번 실행하도록 아래와 같이 설정합니다.
class WordsApplication : Application() {
val applicationScope = CoroutineScope(SupervisorJob())
val database by lazy { WordRoomDatabase.getDatabase(this, applicationScope) }
val repository by lazy { WordRepository(database.wordDao()) }
}
아래 매니페스트에도 추가해야합니다.
<application
android:name=".WordsApplication"
.
.
.
.
NewWordActivity
File - New - Activity - EmptyActivity 를 선택하여 액티비티를 추가합니다.
추가한 뒤 아래와 같이 추가합니다.
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
아래는 activity_new_word.xml 레이아웃입니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:hint="Write word"
android:inputType="textAutoComplete"
android:layout_margin="16dp"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="저장"
android:layout_margin="16dp"
android:textColor="@color/white" />
</LinearLayout>
MainActivity
마지막 메인입니다.
아래와 같이 작성합니다.
FloatingButton 을 누르면 NewWordActivity 로 이동하게 되며 해당 액티비티에서 문자를 입력하고 데이터베이스에 데이터 갱신을 시도합니다.
onActivityResult 함수로 돌아와서 insert 를 실행하여 Database 에 데이터를 Insert 합니다.
데이터 Insert 가 완료되면 onCreate() 내부에서 wordViewModel 이 allWords 데이터 변화를 감지합니다.
데이터가 변화하는 순간 adapter 를 업데이트하여 UI 를 갱신합니다.
class MainActivity : AppCompatActivity() {
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
private val newWordActivityRequestCode = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel.allWords.observe(this) { words ->
words.let { adapter.submitList(it) }
}
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
}
}
}
하나하나 어떻게 동작하는지 확인해봐야 공부가 더 잘되는 것 같네요.
이거는 기초였으며 더 자세하게 공부하려면 조금 더 알아봐야할 것 같네요
'Android > 개념 및 정보' 카테고리의 다른 글
[Android] Hilt 의존성 주입(DI) gradle 버전, 각 어노테이션 설명 (0) | 2020.12.11 |
---|---|
[Context] Fragment 에서 context 와 requireContext (0) | 2020.12.10 |
[Android] Realm Database 첫 시작(with kotlin) (2) | 2020.12.05 |
Android Studio 4.1 업데이트, 새로운 기능과 변경사항 (2) | 2020.10.18 |
[SurfaceView] SurfaceView란, 간단 사용법 (0) | 2020.07.17 |
댓글