보통 사용자가 핸드폰 앱에 저장할 수 있는 곳은
SharedPreference, SQLite, Room DB정도가 있다.
그중 Room DB란 제트팩 라이브러리의 구성요소 중 하나이며
Room 지속성 라이브러리는 SQLite를 완벽히 활용하면서 원활한 데이터베이스 액세스가 가능하도록 SQLite에 추상화 계층을 제공합니다. 특히 Room을 사용하면 다음과 같은 이점이 있습니다.
라며 안드로이드 디벨로퍼에서 강력히 추천한다.
Room은 3가지의 주요 구성요소로 구성된다.
앱은 DAO를 사용하여 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있다.
Android Developer의 RommDB 데이터생성 방법
// 고유하게 식별되도록 하려면 이러한 열을 @Entity의 primaryKeys 속성에 나열하여 복합 기본 키를 정의하면 됩니다.
// @Entity(tableName = ["memo", "note"])
@Entity(tableName = "memo")
data class Memo(
// Room에서 항목 인스턴스에 자동 ID를 할당하게 하려면
// @PrimaryKey의 autoGenerate 속성을 true로 설정
@PrimaryKey(autoGenerate = true)
var id: Int?,
// DB의 (열이름)칼럼명을 변수명과 같이 쓰려면 생략
@ColumnInfo(name = "title")
var title: String,
@ColumnInfo(name = "content")
var content: String,
@ColumnInfo(name = "ispw")
var ispw: Boolean,
@ColumnInfo(name = "pw")
var pw: Int,
@ColumnInfo(name = "date")
var date: String,
) {
constructor() : this(null, "", "",false,-1,"")
}
Android Developer의 RommDB 데이터 엑세스DAO 생성 방법
@QUERY는 SQL문을 작성하여 DB에서 뽑아 올 수 있으며 매개변수를 전달할 때는 ' : ' 콜론을 붙여서 변수를 구분한다.
QUERY 예제
@Query("SELECT * FROM user WHERE first_name LIKE :search " +
"OR last_name LIKE :search")
fun findUserWithName(search: String): List<User>
그외에도
등이 있으니 있다는 것만 알아두고 사용할 때가 되면 Android Developer 참조
MemoDao 예제
@Dao
interface MemoDao {
// memo 테이블의 데이터와 상호작용하는 데 사용하는 메서드를 제공
@Query("SELECT * FROM memo ORDER BY date DESC") // 오름차순 : ACS 내림차순 : DESC
fun getAll(): LiveData<List<Memo>>
@Query("SELECT * FROM memo WHERE title LIKE '%' || :strfind || '%'") // strfind가 들어있는 memo 반환
fun getFilterd(strfind : String?) :LiveData<List<Memo>>
// Id가 중복될 경우 Replace
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(contact: Memo)
@Delete
fun delete(contact: Memo)
}
데이터베이스를 보유할 AppDatabase 클래스를 정의합니다. AppDatabase는 데이터베이스 구성을 정의하고 영구 데이터에 대한 앱의 기본 액세스 포인트 역할을 합니다.
클래스는 RoomDatabase를 확장하는 추상 클래스여야 합니다. -> 즉 RoomDataBase는 인스턴스화가 필요하다.
앱이 단일 프로세스에서 실행되면 AppDatabase 객체를 인스턴스화할 때 싱글톤 디자인 패턴을 따라야 합니다. 각 RoomDatabase 인스턴스는 리소스를 상당히 많이 소비하며 단일 프로세스 내에서 여러 인스턴스에 액세스해야 하는 경우는 거의 없습니다.
그럼으로 RoomDB의 싱글톤 인스턴스와 ViewModel을 이을 때 Apllication단의 context를 전달해 주는 것이 옳다. 만약 이렇게 해주지 않으면 메모리 릭이 발생할 수 있다.
// Database의 entities 는 Memo::class 이며 version은 1이다.
// DB값이 변동되거나 Data Class가 수정되면 version을 고치거나 앱을 지웠다 깔기...
@Database(entities = [Memo::class], version = 1)
abstract class MeMoDatabase: RoomDatabase() {
abstract fun memoDao(): MemoDao
companion object {
private var INSTANCE: MeMoDatabase? = null
fun getInstance(context: Context): MeMoDatabase? {
if (INSTANCE == null) {
synchronized(MeMoDatabase::class) {
INSTANCE = Room.databaseBuilder(context.applicationContext,
MeMoDatabase::class.java, "memo")
.fallbackToDestructiveMigration()
.build()
}
}
return INSTANCE
}
}
}
이로써 RoomDB의 3대 구성요소인 Data Class, Data DAO, DataBase를 모두 만들었다.
이를 어떻게 활용하여 MVVM모델과 연동하여 활용할까?
apply plugin: 'kotlin-kapt'
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.2"
사실 위에 DataDAO에서 이미 LiveData를 반환하고 있음으로 모델 적용은 이미 시작된거라고 할 수 있다.
정확한 MVVM모델과 Room DB의 구조는 이러하다.
Room DB와 ViewModel과의 통신은 그렇다치자. 근데 중간에 Repository란 녀석은 뭘까?
Repository 패턴이란
Repository는 데이터 소스 레이어와 뷰모델 레이어 사이를 중재한다
Repository는 데이터 소스에 쿼리를 날리거나, 데이터를 다른 Doamin에서 사용할 수 있도록 새롭게 mapping 할 수 있다.
레포지토리 클래스는 View Model과 데이터소스를 이어주는 Model클래스 라고 생각하면 된다.
데이터와 ViewModel의 중간에서 Retrofit2와 같은 Web Service도 처리할 수 있다.
나중에 찾아서 해보기
Search Tag : repository mvvm retrofit
MemoRepository
class MemoRepository(application: Application) {
private val memoDatabase = MeMoDatabase.getInstance(application)!!
private val memoDao: MemoDao = memoDatabase.memoDao()
private val memos: LiveData<List<Memo>> = memoDao.getAll()
private var filtermemos : LiveData<List<Memo>> = memoDao.getFilterd("")
fun getAll(): LiveData<List<Memo>> {
return memos
}
fun getFilterMemo(findstr:String): LiveData<List<Memo>> {
try {
val thread = Thread(Runnable {
filtermemos = memoDao.getFilterd(findstr)
})
thread.start()
} catch (e: Exception) { }
return filtermemos
}
fun insert(memo: Memo) {
try {
//ViewModel에서 DB에 접근을 요청할 때 수행할 함수를 만들어둔다.주의할 점은 Room DB를 메인 스레드에서 접근하려 하면 크래쉬가 발생한다
val thread = Thread(Runnable {
memoDao.insert(memo) })
thread.start()
} catch (e: Exception) { }
}
fun delete(memo: Memo) {
try {
val thread = Thread(Runnable {
memoDao.delete(memo)
})
thread.start()
} catch (e: Exception) { }
}
}
Repository를 사용하는 ViewModel
// 만약 ViewModel이 액티비티의 context를 쓰게 되면, 액티비티가 destroy 된 경우에는 메모리 릭이 발생할 수 있다.
// 따라서 Application Context를 사용하기 위해 Applicaion을 인자로 받는다.
class MemoViewModel(application: Application) : AndroidViewModel(application) {
private val repository = MemoRepository(application)
private val memos = repository.getAll()
fun getAll(): LiveData<List<Memo>> {
return this.memos
}
fun getFilterd(findstr : String): LiveData<List<Memo>> {
val filteredmemos = repository.getFilterMemo(findstr)
return filteredmemos
}
fun insert(memo: Memo) {
repository.insert(memo)
}
fun delete(memo: Memo) {
repository.delete(memo)
}
}