막무가내 삽질 블로그
Android Room Database - Kotlin 본문
코드랩과 구글문서를 참조했다.
안드로이드에서 아키텍처 구성요소
해당 예제는 Room, ViewModel, LiveData, Repository 만 사용합니다.
Entity : Room 작업시 데이터베이스 테이블을 설명하는 클래스
DAO : 데이터 접근 객체, SQL 쿼리를 함수에 매핑, DAO를 사용할 때 함수를 부르고 나머지는 room에서 처리한다.
ROOM : SQLite 데이터베이스에 대한 액세스 지점 역할을 한다.
Repository : 여러 데이터 소스를 관리 하는데 사용된다.
ViewModel, LiveData : https://class-programming.tistory.com/75
안드로이드 아키텍처 구성요소를 사용하여 단어장 앱을 만든다.
Entity 만들기
@Entity(tableName = "word_table")
data class Word(
@PrimaryKey
@ColumnInfo(name = "word")
val word: String
)
코드에 보는 거와 같이 데이터 클래스를 생성했다. 기본적으로 단어만 넣을 예정이기 때문에 val word라는 변수하나를 넣었다. @Entity 괄호 안에 tableName을 적었다. 이 경우 내가 테이블이름을 따로 지정할 때 사용한다. @Entity만 사용할 경우는 클래스의 이름이 테이블에 이름이 된다. 알다시피 디비에 저장할때는 고유한 키가 있어야 하는데 그 키가 @PrimaryKey 이고, Room 항목에 자동 ID를 할당하려면 @PrimaryKey의 autoGenerate 속성을 사용하면 된다.
ex) @PrimaryKey(autoGenerate = true) tableName속성과 마찬가지로 Room은 field이름을 데이터베이스의 열 이름으로 사용한다. 열의 이름을 다르게 지정하려면 @ColoumnInfo(name = "필드이름") 주석을 사용하면 된다. 더 많은 속성은 개발자 문서에 더 자세히 나와있다.
Dao 만들기
@Dao
interface WordDao {
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getASCWord(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
데이터베이스에 관한 추상 액세스를 제공하는 메소드가 포함되어 있으며 Room의 주요 구성요소를 형성한다. (쿼리문이라고 생각함) 코드와 같이 테스트앱에는 알파벳 순서로 모든 단어 얻기, 단어 저장, 모든 단어 삭제만 다룰 예정이다. 첫번째 함수는 저장된 단어들을 오름차순으로 가져오는 함수고, 두번째는 단어를 추가시킬 함수고, 세번째는 저장된 단어를 전체 지우는 함수쿼리이다. onConflict = OnConflictStrategy은 몇가지있는데(INSERT 충돌관련) REPLACE는 Insert할 때 PrimaryKey가 겹치는 것이 있으면 덮어 쓴다는 뜻이고 IGNORE는 동일한 단어를 무시한다. 자세한건 공홈에 나와있다.
데이터가 변경 될 때 반응 할 수 있도록 데이터를 관찰해야 하는데 데이터 저장 방법에 따라 까다로울 수 있다. 그래서 LiveData를 사용해 쉽게 해결한다. 위에 코드에서 단어 전체를 가져오는 getASCWord(): List<Word> 부분을 getASCWord(): LiveData<List<Word> 로 바꿔준다.
Room 만들기
@Database(entities = [Word::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun wordDao() : WordDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
val tempInstacne = INSTANCE
if (tempInstacne != null) {
return tempInstacne
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
Room의 데이터베이스 클래스는 abstract여야 한다. @Database 어노테이션에 entities를 설정하고 버전번호를 설정한다. 추상메소드 Dao를 통해 데이터베이스에 제공한다. 싱글톤을 반환하기 때문에 데이터베이스의 다중 인스턴스를 방지할 수 있다.
Repository 만들기
class WordRepository (private val wordDao: WordDao) {
val allWord: LiveData<List<Word>> = wordDao.getASCWord()
suspend fun insert(word: Word) { wordDao.insert(word) }
}
DAO는 데이버 테이스가 아닌 저장소(레포지토리)의 생성자로 전달된다. DAO에는 데이터베이스에 대한 모든 읽기/쓰기 방법이 포함되어 있기 떄문에 DAO에만 액세스 해야 한다. 따라서 전체 데이터베이스를 저장소에 공개 할 필요가 없다. 데이터베이스의 데이터를 가져오면 LiveData가 이를 캐치해서 데이터가 변경되면 메인 스레드 관찰자에게 알려준다.
ViewModel 만들기
class WordViewModel(application: Application) :
AndroidViewModel(application) {
private val repository: WordRepository
val allWord: LiveData<List<Word>>
init {
val wordDao = AppDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordDao)
allWord = repository.allWord
}
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
뷰모델은 Application을 파라미터로 받고 안드로이드뷰모델은 상속받는다. 멤버변수를 추가하기 위해 레포지토리와 LiveData 단어 목록을 캐시하기 위해 allWord를 생성했다. 초기화를 통해 데이터베이스 wordDao에 대한 참조 데이터를 가져온다. insert 함수는 코루틴에 관련있어서 공홈가서 확인해보시길 권장
values/styles.xml
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<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>
layout/recyclerview_item.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="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>
main.xml
<?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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/darker_gray"
tools:listitem="@layout/recyclerview_item"
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
어댑터만들기
class WordListAdapter internal constructor(context: Context) :
RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val view = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(view)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
override fun getItemCount() = words.size
internal fun setWords(word: List<Word>) {
this.words = word
notifyDataSetChanged()
}
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
}
메인액티비티
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
이제 데이터베이스를 수정해줍니다. Appdatabase에 가 getDatabase 함수 파라미터에 context가 있는데 scopre를 파라미터에 추가해 줍니다. fun getDatabase(context: Context, scopre: CoroutineScope)
뷰모델에 가서 파라미터에 viewModelScope를 추가해 줍니다. Appdatabase.getDatabase(application, viewmodelScope).wordDao()
다시 appdatabase로 돌아가 abstract fun wordDao(): WordDao 아래다 아래 코드를 추가해 줍니다.
private class AppDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
wordDao.deleteAll()
var word = Word("HI")
wordDao.insert(word)
word = Word("ggggg")
wordDao.insert(word)
word = Word("ABCD")
wordDao.insert(word)
}
}
}
}
appdatabase 전체코드
@Database(entities = [Word::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class AppDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
wordDao.deleteAll()
var word = Word("HI")
wordDao.insert(word)
word = Word("ggggg")
wordDao.insert(word)
word = Word("ABCD")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(
context: Context, scope: CoroutineScope
): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"word_database"
)
.addCallback(AppDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
res.values/dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="small_padding">6dp</dimen>
<dimen name="big_padding">16dp</dimen>
</resources>
NewWordActivity 만들기
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"
}
}
newword.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:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:padding="@dimen/small_padding"
android:layout_marginBottom="@dimen/big_padding"
android:layout_marginTop="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:textColor="@color/buttonLabel" />
</LinearLayout>
마지막으로 입력한 단어를 저장하고 데이터베이스의 현재 내용을 표시하기 위해 리싸이클러뷰와 연결 시켜야 한다.
메인클래스에서 뷰모델을 정의하고 뷰모델이 데이터변경 되는지를 옵저버를 통해 관찰하고 있다 변경이 있으면 리싸이클러뷰에 연결 시켜주면 된다.
메인액티비티 전체코드
class MainActivity : AppCompatActivity() {
private lateinit var wordViewModel: WordViewModel
private val newWordActivityRequestCode = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWord.observe(this, Observer { words ->
words?.let { adapter.setWords(it) }
})
}
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)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_SHORT
).show()
}
}
}
정리.....
MainActivity : 리싸이클러뷰를 사용하여 단어를 목록에 표시하고, 메인액티비티가 정의한 뷰모델의 옵저버에 LiveData를 관찰하고 변경될 때 어댑터에 통지된다.
NewWordActivity : 새 단어를 추가한다.
ViewModel : 데이터 계층에 액세스하는 방법을 제공하고 LiveData를 반환한다.
LiveData<List<Word>> : 메인 액티비티가 옵저버를 통해 LiveData를 관찰하고 변경될 때 통지된다.
Repository : 하나 이상의 데이터 소스를 관리한다.
Room : wrapper이고 SQLite 데이터베이스를 구현한다.
DAO : 쿼리문
Word : 엔티티 클래스
전체적인 흐름
LiveData를 사용하고 있기 때문에 자동 업데이트가 가능하다. 메인액티비티가 옵저버를 통해 데이터베이스에서 LiveData(데이터)를 관찰하고 변경 될 때 마다 통지된다. 변경 사항이 있으면 옵저버의 onChange()메소드가 실행되고 업데이트된다.
안드로이드 스터디 모집
www.notion.so/fundevjay/Android-ddf96b24265e414fb2d9e8fc5d388b80
'Android' 카테고리의 다른 글
안드로이드 앱 포그라운드, 백그라운드 상태 확인하기 (0) | 2020.04.15 |
---|---|
Room에 저장된 날짜와 현재날짜 비교 (0) | 2020.03.23 |
Android ViewModel + LiveData + Data Binding (0) | 2020.03.01 |
Android Jetpack Navigation (0) | 2020.03.01 |
안드로이드 Room 데이터 베이스 (0) | 2020.02.15 |