이 포스팅은 아래 구글 코드랩을 개인 학습용으로 정리한 글입니다.
관찰할 수 있는 데이터 홀더 클래스
항상 최신 버전의 데이터를 보유/캐시
-> 데이터가 변경된 경우 관찰자에게 알림
LiveData는 수명주기를 인식
-> 관찰하는 동안 관련 수명 주기 상태의 변경 인식 & 자동으로 관리
저장소(데이터)와 UI간의 통신 센터 역할
-> UI에서 데이터의 출처에 관해 걱정하지 않아도 됨
ViewModel 인스턴스는 Activity/Fragment 재생성에도 유지됨
기본 SQLite 데이터베이스의 엑세스 포인터 역할
-> 데이터베이스 작업 간소화
Room 데이터베이스는 DAO를 사용하여 SQLite 데이터베이스에 쿼리 실행
기기 내 저장소
Room 지속성 라이브러리에서 이 데이터베이스를 만들고 유지
데이터 액세스 객체
SQL 쿼리를 함수에 매핑
DAO를 사용할 때 메서드를 호출하면 Room에서 나머지 처리
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
apply plugin: 'kotlin-kapt'
android {
...
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
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:1.7.10"
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"
}
dependencies 블록의 $kotlin_version 사용 중인 코틀린 플러그인의 버전으로 수정
File -> Settings -> Languages & Frameworks -> Kotlin 에서 플러그인 버전 확인 가능
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.0' apply false
id 'com.android.library' version '7.3.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}
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'
}
data class Word (val word:String)
package googlecodelabs.roomwordsample
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "word_table")
data class Word (@PrimaryKey @ColumnInfo(name = "word") val word:String)
@Entity(tableName = "word_table")
- 각 @Entity 클래스는 SQLite 테이블을 나타냄
- 테이블 이름을 클래스 이름과 다르게 하려면 테이블 이름 지정하면 됨
@PrimaryKey
@ColumnInfo(name = "word")
- 테이블의 열 이름을 멤버 변수 이름과 다르게 하려는 경우 열 이름 지정
데이터베이스에 저장된 모든 속성은 공개 가시성(Kotlin 기본값)이 있어야
@Entity(tableName = "word_table")
data class Word (
@PrimaryKey(autoGenerate = true) val id: Int,
@ColumnInfo(name = "word") val word:String
)
DAO(Data Access Object, 데이터 엑세스 객체)
DAO는 인터페이스 또는 추상 클래스여야 함
DAO는 SQL 쿼리를 지정하여 메서드 호출과 연결
컴파일러는 SQL을 확인하고 @Insert와 같은 일반 쿼리의 편의 주석으로 쿼리 생성
Room은 DAO를 사용하여 코드를 위한 깔끔한 API를 만든다
기본적으로 모든 쿼리는 별도의 스레드에서 실행되어야
Room에서는 Kotlin 코루틴 지원
-> 쿼리를 suspend 수정자로 주석 처리
-> 코루틴이나 다른 정지 함수에서 호출 가능
@Dao
interface WordDao {
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
@Dao
- Room의 DAO 클래스로 식별
suspend fun insert(word: Word)
- 한 단어를 삽입하는 정지 함수
@Insert
- SQL을 제공하지 않아도 되는 특수 DAO 메서드 (@Insert, @Delete, @Update)
onConflict = OnConflictStrategy.IGNORE
- 선택된 onConflict 전략: 이미 목록에 있는 단어 삽입하는 경우, 새 단어 무시
suspend fun deleteAll()
- 모든 단어를 삭제하는 정지 함수
- 여러 항목을 삭제하는 편의 주석은 없으므로 일반적인 @Query로 주석처리
@Query("DELETE FROM word_table")
- 문자열 매개변수로 SQL 쿼리를 주석에 제공
- 복잡한 읽기 쿼리와 기타 작업 허용
fun getAlphabetizedWords(): ListWord
- 모든 단어를 가져와서 Words의 List를 반환하는 메서드
@Query("SELECT * FROM word_table ORDER BY word ASC")
- 오름차순으로 정렬된 단어 목록을 반환하는 쿼리
데이터가 변경될 때 대응할 수 있도록 데이터 관찰해야
-> kotlinx-coroutines의 Flow 사용
(FLow는 값의 비동기 시퀸스)
메서드 설명에 Flow 타입의 반환값 사용
-> Room이 데이터베이스가 업데이트될 때 Flow를 업데이트하는 데 필요한 모든 코드 생성
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): Flow<List<Word>>
Room은 SQLite 데이터베이스 위에 있는 데이터베이스 레이어
Room은 개발자가 SQLiteOpenHelper를 사용하여 처리하던 일반적인 작업을 처리
Room은 DAO를 사용하여 데이터베이스에 쿼리 실행
UI 성능 저하 방지를 위해 Room에서는 기본 스레드에서 쿼리 실행 불가능
-> Room 쿼리가 Flow를 반환하면 쿼리는 자동으로 백그라운드 스레드에서 비동기식으로 실행
Room 데이터베이스 클래스는 추상 클래스
-> RoomDatabase 확장해야
일반적으로 전체 앱에 Romm 데이터베이스 인스턴스 하나만 있으면 됨
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
클래스를 Room 데이터베이스가 되도록 @Database로 주석 처리
- 주석 매개변수로 데이터베이스에 속한 항목 선언, 버전 번호 설정
데이터베이스 이전은 이 Codelab의 범위를 벗어남
-> 빌드 경고를 피하기 위해 exportSchema = false로 설정
-> 실제 앱에서는 현재 스키마를 버전 제어 시스템으로 확인할 수 있도록 ⚡스키마를 내보내는 데 사용할 Room 디렉터리를 설정하는 것이 좋다
데이터베이스는 ⚡각 @Dao의 추상 getter메서드를 통해 DAO를 노출
WordRoomDatabase 싱글톤으로 정의
getDatabase는 싱글톤을 반환
- 처음 엑세스할 때 Room의 데이터베이스 빌더 사용
- WordRoodDatabase 클래스의 애플리케이션 컨텍스트에서 RoomDatabase 객체를 만듦
- 이름 "word_database"로 지정
저장소 클래스: 여러 데이터 소스 엑세스를 추상화
아키텍처 구성요소 라이브러리의 일부는 아니지만 코드 분리와 아키텍쳐를 위한 권장사항
저장소는 쿼리 관리 & 여러 백엔드를 사용하도록 허용
가장 일반적인 저장소:
데이터를 네트워크에서 가져올지 or 로컬 데이터베이스에 캐시된 결과를 사용할 지 결정
class WordRepository(private val wordDao: WordDao) {
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
저장소 생성자에 전체 데이터베이스가 아닌 DAO 전달
- DAO에 데이터베이스의 모든 읽기/쓰기 메소드 포함되어 있음
- 전체 데이터베이스를 저장소에 노출할 필요 X
단어 목록은 공개 속성
- Flow 목록을 가져와서 초기화됨
- Room은 별도의 스레드에서 모든 쿼리 실행
suspend
- 코루틴이나 다른 정지함수에서 호출되어야함을 컴파일러에 알림
Room은 기본 스레드 밖에서 정지 쿼리 실행
ViewModel은 수명주기를 고려하여 구성 변경에도 유지되는 앱의 UI 데이터 보유
앱의 UI 데이터를 Activity및 Fragment 클래스에서 분리하면 단일 책임 원칙을 더 잘 준수 가능
- Activity 및 Fragment 클래스: 화면에 데이터를 그리는 것 담당
- ViewModel: UI에 필요한 모든 데이터 보유, 처리
LiveData: 관찰 가능한 데이터 홀더
-> 데이터가 변경될 때마다 알림을 받을 수 있음
FLow와 달리 LiveData는 수명주기 인식
-> 변경을 수신 대기하는 구성요소의 수명 주기에 따라 자동으로 관찰을 중지하거나 재 시작
ViewModel은 저장소의 데이터를 FLow에서 LiveData로 변환
-> 데이터베이스의 데이터가 변경될 때마다 UI가 자동으로 업데이트
class WordViewModel(private val repository: WordRepository) : ViewModel() {
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
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")
}
}
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.RoomWordSample" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="big_padding">16dp</dimen>
</resources>
<?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="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
<?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.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_baseline_add_24"/>
</androidx.constraintlayout.widget.ConstraintLayout>
package googlecodelabs.roomwordsample
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
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)
}
}
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
}
}
}
WordViewHolder
- 텍스트를 TextView에 바인딩
- 레이아웃 확장을 처리하는 정적 create() 함수 노출
WordsComparator
- 두 단어가 동일한 경우/콘텐츠가 동일한 경우 계산하는 방법 정의
WordListAdapter
- onCreateViewHolder에서 WordViewHolder를 만들어 onVBindViewHolder에서 바인딩
앱에 데이터베이스 인스턴스와 저장소 인스턴스를 하나씩만 사용하려고 함
쉬운 방법: 인스턴스를 Application 클래스의 멤버로 생성
-> 매번 구성하지 않고 필요할 때마다 Application에서 가져올 수 있음
class WordsApplication : Application() {
// Using by lazy so the database and the repository are only created when they're needed
// rather than when the application starts
val database by lazy { WordRoomDatabase.getDatabase(this) }
val repository by lazy { WordRepository(database.wordDao()) }
}
데이터베이스 인스턴스를 만듦
저장소 인스턴스를 만듦
- 데이터베이스 DAO에 기반함
이러한 객체는 앱을 시작할 때가 아니라 처음 필요할 때만 만들어져야
-> 코틀린의 속성 위임 by lazy 사용
<application
android:name=".WordsApplication"
...>
현재 데이터베이스에 데이터가 없음
데이터를 추가하는 두 가지 방법:
- 데이터베이스를 만들 때 데이터를 추가
- 단어를 추가하는 Activity 추가
앱을 만들 때마다 모든 콘텐츠를 삭제하고 데이터베이스를 다시 채우려면 RoomDatabase.Callback을 만들고 onCreate() 재정의
-> Room 데이터베이스 작업 UI 스레드에서 할 수 없음
-> onCreate()는 I/O Dispatcher에서 코루틴 실행
코투린을 실행하려면 CoroutineScope 필요
-> 코루틴의 범위로 매개변수로 가져오도록 WordRoomDatabase 클래스의 getDatabase 메서드 업데이트
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
class WordsApplication : Application() {
// No need to cancel this scope as it'll be torn down with the process
val applicationScope = CoroutineScope(SupervisorJob())
val database by lazy { WordRoomDatabase.getDatabase(this, applicationScope) }
val repository by lazy { WordRepository(database.wordDao()) }
}
OnConflictStrategy
https://developer.android.com/reference/androidx/room/OnConflictStrategy.html
https://androidx.de/androidx/room/OnConflictStrategy.html
Set of conflict handling strategies for various Dao methods
- ABORT : abort the transaction
- FAIL : fail the transaction
- IGNORE : ignore the conflict
- REPLACE : replace the old data and continue the transaction
- ROLLBACK : rollback the transaction
- @Volatile
- 변수 선언 시 volatile을 지정하면 메인 메모리에만 적재하게 됨- volatile 변수를 사용하지 않는 일반적인 경우:
- 내부적으로 성능 향상을 위해 메인 메모리로부터 읽어온 값을 CPU 캐시에 저장