본문 바로가기

AndroidCodelabs

Android Room with a View - Kotlin

📍 출처 codelab - https://developer.android.com/codelabs/android-room-with-a-view-kotlin#0

본 포스팅은 google codelabs 과정을 개인적으로 번역하고 필요한 부분만 요약하여 정리한 글입니다! 생략 및 첨언이 있습니다!!

 

 

시작전에

AAC collection은 lifecycle managerment와 data persistence와 같은 공통적인 작업 을위한 라이브러리와 앱아키텍처 가이드를 제공한다 .

[이번 코드랩에서 할것]

  • AAC 이용하여 권장 아키텍처 구현
  • database에서 저잗왼 데이터 받아오기, Sample words 를 받아와서 미리 채우기
  • 모든 단어를 MainActivity에 recycler view로 보여주기
  • +button 누르면 SecondActivity 열리고, 사용자가 단어 입력시 DB에 추가하고 RecyclerView에 추가하기

Architecture Components 사용하기

  • LiveData

data를 담아두는 holder class 로 observe 할 수 있다. 항상 최신화된 데이터를 담아두고 캐싱한다. 그리고 LiveData를 observers 에게 데이터가 변경되면 notify 한다. LiveData는 lifecycle을 알고있다. UI component들은 관련있는 데이터를 observe만 하고 observe를 중지하거나 다시시작하진 않는다. LiveData는 observing하는동안 lifecycle의 상태 변경을 인식하고 자동으로 관리한다.

  • ViewModel

Repository와 UI 사이에서 communication center 역할을 한다. UI는 더이상 걱정할 필요가 없다 데이터에 출처에 대해서. ViewModel 인스턴스는 Activity Fragment 재생성에도 살아남는다.

  • Repository

주로 여러 DataSource를 관리하는데 사용되는 클래스

  • Entity

Room 사용할때 Database Table을 구성하기위한 Annotation class

  • Room database

database 작업을 간편하게 하고 SQLite 데이터 베이스의 엑세스 포인트 역할을 한다. Room은 DAO를 를 이용하여 DB에 쿼리한다

  • SQLite DB

device storage다. Room persistence library는 database 만들고 유지한다.

  • DAO

Data access object로 SQL 쿼리를 함수로 mapping한다. DAO 사용하여 함수 호출 하면 남은 건 Room이 알아서 한다.

 

 

구현할 Sample app 구조

SQLite영역 제외하고 구현할거당~

 

Entity 만들기

app을 위한 데이터는 words이고, 이 값들을 위해 간단한 table 이 필요하다

Word 데이터를 담기위한 Word Data Class 를 생성. 이 클래스는 Word 테이블의 구조를 설명하기위한 클래스. 해당 data class의 property는 table의 column를 나타낸다 . Room은 Entity DataClass를 2가지로 사용하는데, 하나는 테이블 생성, 하나는 DB의 행을 나타내는 인스턴스화된 객체로 사용

@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

 

DAO 만들기

  • DAO는 인터페이스나 추상클래스여야한다
  • 모든 쿼리는 별도 스레드에서 실행되어야한다
  • Room 은 코루틴을 지원, suspend function으로 가능
@Dao
interface WordDao {

   @Query("SELECT * FROM word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): Flow<List<Word>>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(word: Word)

    @Query("DELETE FROM word_table")
    suspend fun deleteAll()
}
  • Insert에서 onConflict는 같은 데이터가 insert되어 충돌될때 대응 방식을 지정한다. 위는 새단어 무시
  • database 변경사항 observing하기 → 데이터 변경시 대응을 위해 데이터를 observe하기 위해서는 kotlin croroutines에 Flow를 사용해야한다. DAO method에 return type을 Flow로 사용하면된당

Room Database 추가

간단 설명

  • Room은 SQLite database의 상위 layer다
  • UI 퍼포먼스 poor을 피하기 위해 메인스레드에서 쿼리하면 안된당. query method에 return을 Flow로 하면 알아서 background Thread에서 비동기적으로 수행해준당
  • 컴파일 타임에 검사해줘서 runtime 오류 방지가능

구현하기

  • database class는 abstract 클래스여야하고 RoomDatabase를 상속하야한다. 대부분 한 앱에서 Database 는 하나만 필요
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   companion object {
        // Singleton prevents multiple instances of database opening at the
        // same time. 
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            // 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,
                        WordRoomDatabase::class.java, 
                        "word_database"
                    ).build()
                INSTANCE = instance
                // return instance
                instance
            }
        }
   }
}
  • entities로 사용할 테이블들 나열
  • 데이터 베이스 스키마 수정시 version번호를 업데이트하고 업데이트를 위한 쿼리등을 정의해야하던데.. 이전에 디바이스에서 이미 생성되어있는 데이터베이스 버전과 비교해서 버전이 달라지면 알아서 업데이트를 위한 쿼리들을 실행해주는 모양이다.
  • 앱에서 데이터베이스 인스턴스 한개보장을 위해 싱글톤으로 생성

Repository 생성

repository는 AAC의 일부는 아니지만 아키텍처를 위해 권장되는 구조이다. repository는 여러 DataSource 접근을 결정한다. 네트워크를 위한 DataSrource를 이용하여 데이터를 받을지, 로컬 DB에 캐시된값을 받아올지 등에 대한 결정로직들이 들어감

// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {

    // Room executes all queries on a separate thread.
    // Observed Flow will notify the observer when the data has changed.
    val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()

    // By default Room runs suspend queries off the main thread, therefore, we don't need to
    // implement anything else to ensure we're not doing long running database work
    // off the main thread.
    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}
  • DAO를 Repository에 전달
  • allWords는 Flow로 처음 repository생성시 데이터를 조회하여 초기화 된다 .

ViewModel 만들기

viewModel의 역할은 구성변경에도 살아남고(유지되고), UI에 데이터를 공급하는것이다.

Repository와 UI사이의 communitation 역할을 한다 .

Fragment들간에 데이터 공유 역시 ViewModel로 할 수 있다.

lifecycle library에 속해있다

ViewModel을 사용하는 이유

  • ViewModel은 앱의 UI 데이터를 담고있다. lifecycle을 고려하는 방식으로 configutation이 변경되어도 살아남도록
  • 앱의 UI데이터를 Activity와 Fragment에서 분리하여 단일책임원칙을 지키는것이 좋다
  • activity나 fragment는 스크린에 데이터 보여주는것만 책임지고, viewModel은 데이터 유지 및 UI를 위한 데이터 연산만 하는게 좋다

LiveData와 ViewModel

  • LiveData는 Flow와 달리 Activity나 Fragment같은 components의 lifecycle을 따라간다. (생명주기안다)
  • LiveData는 데이터 변경을 구독하고있는 component의 lifecycle에 의존하여 자동으로 observe하는것을 멈주거나 resume한다.
  • 위 특성은 LiveData를 UI가 사용하고, Display할 변경가능한 데이터로 사용하기에 완벽한 컴포넌트로 만든다.
  • ViewModel은 repository에서 받아온 데이터를 Flow에서 LiveData로 변경하여 UI쪽에 배포 노출할거야.
  • 이건 Database의 데이터가변경되면 매순간 UI가 자동으로 update되는걸 보장할꺼야!

viewModelScope

  • 코틀린에서 모든 코루틴은 코루틴 스코프 안에서 동작한다.
  • Scope는 Job을 통해 코루틴의 lifetime을 컨트롤한다
  • scope의 job을 cancel하면 스코프 내부의 모든 코루틴들이 취소된다
  • viewModelScope - lifecycle-viewmodel-ktx 추가해야해
class WordViewModel(private val repository: WordRepository) : ViewModel() {

    // Flow를 LiveData로 변경 
    val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()

		// UI쪽에서 scope사용할 필요가 없어짐
    fun insert(word: Word) = viewModelScope.launch {
        repository.insert(word)
    }
}

// ViewModel의 생명주기 고려해서 ViewModel관리하기위한 Factory 
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")
    }
}
  • ViewModel보다 생명주기가 짧은 context 참조 하지말것 (ex. activity)
  • 앱이 리소스 부족으로 중단되면 ViewModel도 사라짐, 이런경우도 유지되어야하면 onSaveInstanceState등을 사용하여 유지가능

RoomDatabase.Callback 구현

app을 시작할때 데이터베이스의 데이터를 모두 삭제한다던가 하는 초기 작업이 필요한경우 RoomDatabase.Callback의 onCreate를 override해야한다.

이때 DB작업은 IO Dispatcher로실행하도록 한다. 코루틴 스코프 가 필요하기 때문에 Database 클래스에 companion object안에 싱글톤으로 객체반환하도록 생성한 getDatabase 함수에 인자로 스코프를 추가해야한다.

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

callback 을 구현하고, database 클래스 인스턴스 생성에 callback 연결까지 적용한 최종 코드

@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   private class WordDatabaseCallback(
       private val scope: CoroutineScope
   ) : RoomDatabase.Callback() {

       override fun onCreate(db: SupportSQLiteDatabase) {
           super.onCreate(db)
           INSTANCE?.let { database ->
               scope.launch {
                   var wordDao = database.wordDao()

                   // Delete all content here.
                   wordDao.deleteAll()

                   // Add sample words.
                   var word = Word("Hello")
                   wordDao.insert(word)
                   word = Word("World!")
                   wordDao.insert(word)

                   // TODO: Add your own words!
                   word = Word("TODO!")
                   wordDao.insert(word)
               }
           }
       }
   }

   companion object {
       @Volatile
       private var INSTANCE: WordRoomDatabase? = null

       fun getDatabase(
           context: Context,
           scope: CoroutineScope
       ): WordRoomDatabase {
            // 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,
                        WordRoomDatabase::class.java,
                        "word_database"
                )
                 .addCallback(WordDatabaseCallback(scope))
                 .build()
                INSTANCE = instance
                // return instance
                instance
        }
     }
   }
}

DataBase Instance 생성

class WordsApplication : Application() {
    // No need to cancel this scope as it'll be torn down with the process
    val applicationScope = CoroutineScope(SupervisorJob())

    // 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, applicationScope) }
    val repository by lazy { WordRepository(database.wordDao()) }
}
  • 처음 해당 인스턴스들이 접근될때 생성되어 Application이 살아있는동안 해당 instance를 사용한다.

Room과 관련된 코드만 정리해보았고 전체코드는 googlecodelabs의 git hub에서 확인할 수 있다.

 

GitHub - googlecodelabs/android-room-with-a-view

Contribute to googlecodelabs/android-room-with-a-view development by creating an account on GitHub.

github.com

 

끝~~