IT 프로그래밍-Android

[데이터바인딩] Room 데이터베이스 적용하기

godsangin 2020. 5. 14. 14:31
반응형

지난 시간까지 안드로이드 데이터바인딩에 대해 실습해보았습니다. 이번 시간에는 Room 라이브러리를 통해 데이터베이스를 정의하고 애플리케이션 내에서 영구적으로 존재하는 데이터를 생성하는 실습을 진행해보도록하겠습니다.

 

https://in-idea.tistory.com/37

 

[안드로이드] 데이터바인딩 적용하기

안녕하세요. 이번시간에는 안드로이드 MVVM 디자인 패턴을 적용하기 위한 데이터바인딩에 대해 알아보려고 합니다. MVVM 패턴을 적용하면 View와 Model간의 의존성을 최소화할 수 있고, View와 관련된

in-idea.tistory.com

https://in-idea.tistory.com/38

 

[데이터바인딩] RecyclerView와 BindingAdapter

지난 시간에 이어서 데이터바인딩 실습을 진행해보도록 하겠습니다. 오늘은 저번 실습인 텍스트 붙히기 예제를 RecyclerView의 item으로 추가하는 예제를 준비해봤습니다 ! 우선 RecyclerView를 사용하�

in-idea.tistory.com

우선 첫번째로 gradle에 데이터베이스 라이브러리인 room과 비동기작업을 처리하기 위한 coroutine라이브러리를 추가합니다.

def room_version = "2.2.3"
kapt "androidx.room:room-compiler:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
kapt "android.arch.persistence.room:testing:$room_version"
implementation "androidx.room:room-runtime:$room_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

gradle빌드를 다시 하고 memo클래스를 room에서 사용하기 위해 아래와 같이 변경합니다.

package com.myhome.viewmodelsample.model

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

@Entity(tableName = "memo")
data class Memo(@PrimaryKey(autoGenerate = true) var id:Long, var text:String)

다음으로 사용할 데이터베이스를 정의하기 위해 db패키지에 아래의 세 클래스를 추가합니다.

package com.myhome.viewmodelsample.db

import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Update

interface BaseDao<T> {
    @Insert(onConflict = OnConflictStrategy.ABORT)
    fun insert(obj : T)

    @Delete
    fun delete(obj : T)

    @Update(onConflict = OnConflictStrategy.ABORT)
    fun update(obj : T)
}

<BaseDao.kt>

package com.myhome.viewmodelsample.db

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Query
import com.myhome.viewmodelsample.model.Memo

@Dao
interface MemoDao :BaseDao<Memo>{
    @Query("SELECT * FROM memo WHERE text = :text")
    fun getMemo(text:String):Memo?

    @Query("SELECT * FROM memo")
    fun getAllMemoSyn():List<Memo>?

    @Query("SELECT * FROM memo")
    fun getAllMemoAsyn():LiveData<List<Memo>>?

}

<MemoDao.kt>

package com.myhome.viewmodelsample.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.myhome.viewmodelsample.model.Memo

@Database(entities = arrayOf(Memo::class), version = 1, exportSchema = false)
abstract class AppDatabase :RoomDatabase(){
    abstract fun memoDao():MemoDao

    companion object{
        private var INSTANCE:AppDatabase? = null
        fun getInstance(context: Context): AppDatabase? {
            if (INSTANCE == null) {
                synchronized(AppDatabase::class) {
                    INSTANCE = Room.databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java,
                        "memo.db"
                    ).build()
                }
            }
            return INSTANCE
        }
    }
}

<AppDatabase.kt>

 

Room라이브러리는 기본적으로 SQLite를 확장하여 추상화 레이어를 제공합니다. 때문에 BaseDao에 존재하지 않는 쿼리문을 적용하기 위해서는 위와 같이 MemoDao와 같은 확장클래스를 새로 추가해야합니다.

 

위의 MemoDao에 정의된 getAllMemoSyn와 getAllMemoAsyn의 차이를 아시나요 ?? 

같은 쿼리문이여도 반환하는 자료형의 형태에 따라 동기/비동기처리를 따로 정의할 수 있습니다. 동기 처리 방식은 백그라운드 작업에서만 수행할 수 있는 반면 LiveData의 경우 Observer를 통하여 비동기 방식으로 데이터를 반환받을 수 있습니다. 또한 데이터의 변화를 감지하고 실시간으로 최신의 데이터를 반영할 수 있도록 할 수 있습니다. 예를 들어 방금 전 추가된 메모를 화면에 추가하기 위해서는 데이터베이스에서 동기 데이터요청을 다시 한번 수행해야 하지만 livedata의 경우 데이터바인딩과 마찬가지로 초기에 한번만 Observer를 통해 정의하면 자동으로 추가된 데이터를 반영할 수 있습니다.

 

다음은 변화된 MainViewModel입니다.

package com.myhome.viewmodelsample.viewmodel

import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableField
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.myhome.viewmodelsample.db.AppDatabase
import com.myhome.viewmodelsample.model.Memo
import com.myhome.viewmodelsample.view.MainViewModelListener

class MainViewModel(listener:MainViewModelListener) {
    val listener = listener
    val myText = ObservableField<String>()
    var index:Long = 0
    val memoList = ObservableArrayList<Memo>()

    fun addMemo(){
//        memoList.add(Memo(index++, myText.get().toString()))
//        myText.set("")
        listener.addMemo(Memo(0, myText.get() ?: "null"))
        myText.set("")
    }

    fun loadMemo(database:AppDatabase?, owner:LifecycleOwner){
        database?.memoDao()?.getAllMemoAsyn()?.observe(owner, Observer {
            memoList.clear()
            for(memo in it){
                memoList.add(memo)
            }
        })
    }
}

새로운 함수와 객체가 많이 생겼죠 ? 이는 activity에게 이벤트를 전달할 listener와 이전에 생성한 AppDatabase, livedata를 수신할 생명주기를 갖는 lifecyclerOwner입니다.

MainActivity를 다음과 같이 변경합니다.

package com.myhome.viewmodelsample.view

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import com.myhome.viewmodelsample.R
import com.myhome.viewmodelsample.databinding.ActivityMainBinding
import com.myhome.viewmodelsample.db.AppDatabase
import com.myhome.viewmodelsample.model.Memo
import com.myhome.viewmodelsample.viewmodel.MainViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    var database:AppDatabase? = null
    val listener = object:MainViewModelListener{
        override fun addMemo(memo: Memo) {
            CoroutineScope(Dispatchers.IO).launch {
                database?.memoDao()?.insert(memo)
            }
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        database = AppDatabase.getInstance(applicationContext)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        val viewModel = MainViewModel(listener)
        binding.model = viewModel
        viewModel.loadMemo(database, this)
    }
}

 

AppDatabase를 초기화하고, viewModel에서 데이터를 수신하기 위한 함수를 호출합니다.

viewModel을 통해 addMemo이벤트가 호출될 경우 listener의 addMemo에서 memo를 추가합니다. 데이터베이스 추가 이벤트는 백그라운드 환경에서만 가능하기 때문에 Coroutine의 I/O scope를 활용합니다.

마지막으로 MainViewModelListener인터페이스를 생성합니다.(원래 처음에 했어야...)

package com.myhome.viewmodelsample.view

import com.myhome.viewmodelsample.model.Memo

interface MainViewModelListener{
    fun addMemo(memo:Memo)
}

 

이와 같이 영구적인 데이터를 보관할 Room데이터베이스를 생성할 수 있습니다. Room라이브러리는 새로운 클래스(테이블)가 생성되거나 기존 클래스(테이블)가 삭제되면 따로 마이그래이션을 해야합니다. 때문에 초기에 데이터모델링을 잘 수행해야 합니다.(저의 경우에도 새로 클래스를 추가하는 과정에서 애플리케이션을 몇번이나 삭제했습니다...애플리케이션을 삭제할 경우 마이그래이션 없이도 테이블을 재정의할 수 있지만 기존의 데이터가 다 사라지게 됩니다.)

 

또한 여러 테이블을 사용하고 싶을 경우 아래의 NewDao와 NewOne이라는 클래스를 MemoDao, Memo클래스처럼 정의하면 됩니다.

package com.myhome.viewmodelsample.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.myhome.viewmodelsample.model.Memo

@Database(entities = arrayOf(Memo::class, NewOne::class), version = 1, exportSchema = false)
abstract class AppDatabase :RoomDatabase(){
    abstract fun memoDao():MemoDao
	abstract fun newDao():NewDao
    companion object{
        private var INSTANCE:AppDatabase? = null
        fun getInstance(context: Context): AppDatabase? {
            if (INSTANCE == null) {
                synchronized(AppDatabase::class) {
                    INSTANCE = Room.databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java,
                        "memo.db"
                    ).build()
                }
            }
            return INSTANCE
        }
    }
}