앞의 글을 참조해 주세요.
[Android/이론] - MVVM 시작하기(1) ― LiveData, Room을 MVVM패턴으로 사용해보자
먼저, 데이터를 저장할 Room의 Entity, Dao, Database를 만들어 주겠습니다.
Room
- 종속성 추가
apply plugin: 'kotlin-kapt'
...
dependencies{
...
// Room components
def roomVersion = '2.2.5'
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion"
// Kotlin components
def coroutines = '1.3.4'
def kotlin_version = "1.3.72"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
}
Room의 종속성 추가와 함께, Coroutine 사용을 위해 Coroutine 종속성도 추가해주었습니다.
- UserEntity.kt
@Entity(tableName = "user_table")
data class UserEntity(
@PrimaryKey
@ColumnInfo(name="name")
val name: String,
@ColumnInfo(name="gender")
val gender: String?,
@ColumnInfo(name="birth")
val birth: String?
)
PrimaryKey를 자동적으로 생성해주게 하려면 다음처럼 사용할 수 있습니다.
@PrimaryKey(autoGenerate = true)
val id: Int
PrimaryKey는 Table 내의 식별자라고 생각하시면 됩니다.
- UserDao.kt
@Dao
interface UserDao {
@Query("SELECT * from user_table ORDER BY name ASC")
fun getAlphabetizedUsers(): LiveData<List<UserEntity>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(userEntity: UserEntity)
@Query("DELETE FROM user_table")
suspend fun deleteAll()
}
- getAlphabetizedUsers()를 통해 "user_table"내의 "name" 컬럼 기준으로 모든 UserEntity를 오름차순(ASC) 정렬하여 가져옵니다. (*DESC: 내림차순)
- return 값 List<UserEntity>를 LiveData로 감싸주어 변화를 감지합니다.
- insert 메서드를 통해 UserEntity에 데이터를 삽입할 수 있습니다.
- onConflict = OnConflictStrategy.IGNORE 의 뜻은 같은 값이 들어왔을 때 무시한다는 뜻입니다.
Constants | |
ABORT | OnConflict strategy constant to abort the transaction. |
FAIL | OnConflict strategy constant to fail the transaction. |
IGNORE | OnConflict strategy constant to ignore the conflict. |
REPLACE | OnConflict strategy constant to replace the old data and continue the transaction. |
ROLLBACK | OnConflict strategy constant to rollback the transaction |
- insert, deletAll 메서드는 데이터베이스의 추가/삭제 이므로, 코루틴 스코프(또는 스레드)에서 사용해주어야 합니다. 코루틴 스코프에서 사용하기 위해 suspend를 붙여주었습니다.
- 참고
- update, delete
@Update
fun update(user: UserEntity);
@Delete
fun delete(user: UserEntity);
- id로 데이터 가져오기
@Query("select * from tool_table where id = :toolId")
suspend fun loadTool(toolId: Int): ToolEntity
- 특정 필드 업데이트
@Query("UPDATE user_table SET name = :name WHERE id = :id")
suspend fun updateName(id: Int, name: String)
- 특정 단어가 포함된 데이터 가져오기
@Query("SELECT * FROM hamster WHERE name LIKE :search")
fun loadHamsters(search: String?): Flowable<List<Hamster>>
- 쿼리 검색
@Query("SELECT tool_table.* FROM tool_table JOIN toolsFts ON (tool_table.name = toolsFts.name) WHERE toolsFts MATCH :query")
fun searchAllTools(query: String?): LiveData<List<ToolEntity>>
[Android/Function] - [안드로이드] Room 데이터베이스의 검색기능 구현하기 ― Room Fts4
- AppDatabase.kt
@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): AppDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addCallback(AppDatabaseCallback(scope))
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
// return instance
instance
}
}
}
private class AppDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.userDao())
}
}
}
suspend fun populateDatabase(userDao: UserDao) {
//userDao.deleteAll()
// Add User
userDao.insert(UserEntity("Lilly","여","1993-07-25"))
}
}
}
- Database class는 추상클래스, Singletone으로 구현되어야 합니다.
- 일반적으로 Database는 전체 앱에 하나의 인스턴스만 만듭니다.
- .addCallback(AppDatabaseCallback(scope))
RoomDatabaseCallback을 만들어 onCreate()을 override 하여 데이터베이스가 처음 생성되었을때 할 행동을 코딩할 수 있습니다.
onOpen()를 override하면 데이터베이스가 열릴때마다 할 활동을 만들 수 있습니다.
onCreate에서 데이터베이스에 처음 데이터를 넣어주고, 이를 addCallback()을 통해 추가해주었습니다.
insert는 데이터의 추가이므로 coroutine을 사용하였습니다.
- .fallbackToDestructiveMigration()
본래 database의 schema가 바뀌었을 경우 version을 변경해 주어야 합니다.
@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
위의 annotation을 확인해주세요.
dao나 entity등이 변화했을 경우 version을 2로 바꾸어주고, migration을 진행해야 합니다.
따로 이전 데이터의 저장 없이 즉, migration 구현 없이 전의 데이터를 지우고 새로운 버전을 시작하기 위해 fallbackToDestructiveMigration()을 추가하였습니다.
- exportSchema = false
true로 하면 Room의 schema를 json파일로 생성할 수 있습니다.
- entities = [UserEntity::class]
entity가 여러개일 경우, array에 entity를 더 추가해 주면 됩니다.
.getDatabaseCreated를 이용하여 생성된 데이터베이스를 가져오는 메서드도 추가 할 수 있습니다.
private val mIsDatabaseCreated = MutableLiveData<Boolean>()
...
/**
* Check whether the database already exists and expose it via [.getDatabaseCreated]
*/
private fun updateDatabaseCreated(context: Context) {
if (context.getDatabasePath(DATABASE_NAME).exists()) {
setDatabaseCreated()
}
}
private fun setDatabaseCreated() {
mIsDatabaseCreated.postValue(true)
}
open fun getDatabaseCreated(): LiveData<Boolean> {
return mIsDatabaseCreated
}
getDatabase()에서 인스턴스가 생셩될 때 아래 코드를 추가해주면 됩니다.
INSTANCE?.updateDatabaseCreated(context.applicationContext)
- 최종 예제 코드 참조
@Database(
entities = [UserEntity::class, ToolEntity::class, ToolFtsEntity::class],
version = 8,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun toolDao(): ToolDao
private val mIsDatabaseCreated = MutableLiveData<Boolean>()
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
private const val DATABASE_NAME = "app_database"
fun getDatabase(
context: Context,
scope: CoroutineScope
): AppDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DATABASE_NAME
)
.addCallback(AppDatabaseCallback(scope))
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
INSTANCE?.updateDatabaseCreated(context.applicationContext)
// return instance
instance
}
}
}
private class AppDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.toolDao())
}
}
}
suspend fun populateDatabase(toolDao: ToolDao) {
toolDao.deleteAll()
val products: List<ToolEntity> = DataGenerator.generateTools()
for(p in products){
toolDao.insert(p)
}
}
}
/**
* Check whether the database already exists and expose it via [.getDatabaseCreated]
*/
private fun updateDatabaseCreated(context: Context) {
if (context.getDatabasePath(DATABASE_NAME).exists()) {
setDatabaseCreated()
}
}
private fun setDatabaseCreated() {
mIsDatabaseCreated.postValue(true)
}
open fun getDatabaseCreated(): LiveData<Boolean> {
return mIsDatabaseCreated
}
}
이어서, Room을 ViewModel, Repository, View와 연결해 보겠습니다.
[Android/이론] - MVVM 시작하기(3) ― ViewModel, 데이터 바인딩(Data Binding)
'Android > Architecutre' 카테고리의 다른 글
[안드로이드 MVVM] Repository 에서 ViewModel, View(Activity,Fragment)에 Event/Data 전달하기 ― Repository에서의 LiveData ? (0) | 2021.01.25 |
---|---|
MVVM 시작하기(4) ― Repository, Model-View 연동 (5) | 2020.07.31 |
MVVM 시작하기(3) ― ViewModel, 데이터 바인딩(Data Binding) (0) | 2020.07.30 |
MVVM 시작하기(1) ― LiveData, Room을 MVVM패턴으로 사용해보자 (0) | 2020.07.28 |