관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] Kotlin Architecture Components (안드로이드 코틀린 아키텍처 구성요소) 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] Kotlin Architecture Components (안드로이드 코틀린 아키텍처 구성요소)

막무가내막내 2020. 2. 22. 15:19
728x90

 

[2021-04-13 업데이트]

 

안드로이드 개발자 문서만 보고 공부하면 이해가 안 되거나 감이 안잡히는 부분들도 있어 코드랩과 코드를 보면서 공부하는 중이다.

 

출처는 다음과 같다.

코드랩 : Android Room with a View - Kotlin

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0

 

Android Room with a View - Kotlin

Your Room database class must be abstract and extend RoomDatabase. Usually, you only need one instance of a Room database for the whole app. Let's make one now. Create a Kotlin class file called WordRoomDatabase and add this code to it: In Android Studio,

codelabs.developers.google.com

 

 

블로그 포스팅을하면 이해와 기억이 더 잘되는 면도 있어서 차례대로 공부내용을 정리해볼려고 한다.

 

 

1. Introduction

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0

 

Architecture Components의 목적은 앱 아키텍처에 대한 가이드를 lifecycle 관리 및  data persistence과 같은 일반적인 작업을 위한 라이브러리와 함께 제공하는 것이다. Architecture Component는  Android Jetpack에 속해있다. 

 

Architecture components는 앱이 더 적은 boilerplate code로 더 견고하고 테스트가 좋고 유지보수 하는데 도움이 된다.

 

여기서 boilerplate code란 프로그래밍으로는 상용구 코드를 의미한다. 즉 수정하지 않거나 최소한의 수정만을 거쳐 여러 곳에 필수적으로 사용되는 코드를 말한다. 이와 같은 코드는 최소한의 작업을 하기 위해 많은 분량의 코드를 작성해야 하는 언어에서 자주 사용된다.

 

예를들어 자바는 getter, setter같이 거의 필수적으로 모든 데이터 클래스에 작성하는것을 lombok을 통해 줄일 수 있다.

코틀린의 경우는 lombok이 따로 필요없이 data class로 해결이 된다.

 

 

 

*권장되는 Architecture Components란?

Entity: Room으로 작업할 때 데이터베이스 테이블을 묘사하는 annotated 클래스이다.

 

SQLite database: device storage에서, Room 지속성 라이브러리는 이 데이터베이스를 만들어지고 유지관리 해준다.

 

DAO: Data access object를 의미한다. 함수에 SQL query를 맵핑해준다. DAO를 사용할 떄, 메소드를 호출하면 나머지는 룸에서 처리해준다. 

 

Room database: 데이터베이스 작업을 단순화시켜준다. 룸 데이터베이스는 기본 SQLite database에 access point(진입점)  역할을 한다. 이때 DAO를 SQLite database에 쿼리를 시행해주기위해 사용한다.

 

Repository: 여러 data sources를 관리하는데 주로 사용되기 위해 생성하는 클래스이다.

 

ViewModel: Repository (data)와 UI 사이의 communication center로서의 역할을 한다. UI는 더이상 데이터의 출처에 대해 생각할 필요가 없어진다. ViewModel 인스턴스는 Activity/Fragment recreation 후에도 유지된다. 

 

LiveData: 관찰할 수 있는 data holder class 이다.  항상 최신 버전의 데이터를 보관/캐시하고 데이터가 변경되었을 때 관찰자에게 통지해준다. LiveData는 수명주기를 인식한다. UI components는 관련 데이터 만 관찰하며 관찰을 중지하거나 다시 시작하지 않는다. LiveData는 관찰하는 동안 관련 수명주기 상태 변경을 인식하므로 이 모든 것을 자동으로 관리한다.

 

 

 

예시로 만들 샘플은 RoomWordSample  라는 앱이다.

  • Works with a database to get and save the data, and pre-populates the database with some words.
  • Displays all the words in a RecyclerView in MainActivity.
  • Opens a second activity when the user taps the + button. When the user enters a word, adds the word to the database and the list.

 

아키텍처는 다음과 같이 구성되어있다.

Architecture Components Room and Lifecycles libraries 를 사용하는 법을 배우게 된다.

안드로이드 버전이 3.1 이상이여야 한다.

 

다음 기본지식이 필요하다.

[Note that the solution code is available as a zip file or github repo. We encourage you to create the app from scratch and look at this code only if you get stuck]

 

 

 

 


 

2. 기본세팅 (Gradle file setting) 

->현재는 버전이 높아져 Gradle 세팅 방식이 다를 수 있으므로 공식 홈페이지를 참고 하길 추천한다.

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#2

 

[module 수준]

kapt 어노테이션 프로세서 코틀린 플러그인을 추가해준다.

apply plugin: 'kotlin-kapt'

 

 

packagingOption을 android 블록안에 추가해준다. 패키지에서  atomic functions module 을 패키지로부터 배제하고 경고를 방지한다.

android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}

 

 

dependencies 블록에 다음을 추가 해준다.

// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
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-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.androidxArchVersion"

// ViewModel Kotlin support
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"

// Coroutines
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

// UI
implementation "com.google.android.material:material:$rootProject.materialVersion"

// Testing
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"

 

 

[project 수준]

다음과 같이 버전을 추가한다.

ext {
    roomVersion = '2.2.1'
    archLifecycleVersion = '2.2.0-rc02'
    androidxArchVersion = '2.1.0'
    coreTestingVersion = "2.1.0"
    coroutines = '1.3.2'
    materialVersion = "1.0.0"
}

 

 

 

 


 

 

 

 

3. 엔티티 생성 (Create Entity)

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#3

 

이 샘플 앱의 data는 word이다. 다음과 같다.

 

Room은 엔티티를 통해 테이블을 생성할 수 있게 해준다.

 

코틀린의 data class를 통해 Word data class 를 생성해준다.

이 클래스는 단어들을(words) 위한 엔티티를 나타낸다. (SQLite table을 나타내는)

클래스에서 각각의 public property(변수)는 테이블의 컬럼을 나타낸다. Room은 궁극적으로 이러한 property 들을 사용하여 테이블을 생성하고 데이터베이스의 행으로부터 객체를 인스턴스화한다. 

 

data class의 기본 코드는 다음과 같다.

data class Word(val word: String)

 

이 Word 클래스를 Room database에 meaningful하기 만들려면(Room의 기능을 사용할려면) 어노테이션을 추가해야한다. 어노테이션은 이 class의 각 부분이 데이터베이스의 항목과 어떻게 관련되는지 식별한다. Room은 이 정보를 사용해서 코드를 생성한다.

 

만약 내가 어노테이션을 작성하면, 안드로이드 스튜디오는 어노테이션 클래스들을 자동으로 import 해준다. 

이러한 어노테이션을 적용한 코드는 다음과 같다.

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

(If you pasted the code in, you can move the cursor to each highlighted error and use the "Project quick fix" keyboard shortcut (Alt+Enter on Windows/Linux, Option+Enter on Mac) to import classes quickly.)

 

 

어노테이션의 역할에 대해 알아보자.

  • @Entity(tableName = "word_table")
    각각의@Entity class는 SQLite 테이블을 의미한다. 내 클래스선언에 어노테이션을 붙임으로서 이를 entity임을 알게한다. 만약 클래스의 이름과 다르게 테이블 명을 짓고 싶다면 위와 같이 tablName="word_table" 이라고 어노테이션에 적어주면 된다.  
  • @PrimaryKey
    모든 Entity는 primary key가 필요하다. primary key에 해당하는 키에 적용해주면 된다. 각각의 단어는 당연히 각각 서로 다른 Primaty Key를 갖게 될 것이다.
  • @ColumnInfo(name = "word")
    변수명과 다른 테이블 컬럼명을 사용하고 싶으면 이 어노테이션을 사용하면 된다. 따로 사용안하면 테이블의 컬럼명은 변수명과 똑같이 된다.
  • 데이터베이스에 저장되는 모든 property는 public 접근자로 해줘야한다. (코틀린은 default로 public으로 되어있다.)

 

 

 

[참고할것 ]

See Defining data using Room entities.

 

Room 항목을 사용하여 데이터 정의  |  Android 개발자  |  Android Developers

Room 라이브러리의 일부인 항목을 사용하여 데이터베이스 테이블을 생성하는 방법 알아보기

developer.android.com

 

 

Primary key를 밑과 같이 autoGenerate를 통해 unique한 키를 자동생성 해줄 수도 있다.

@Entity(tableName = "word_table")
public class Word {

@PrimaryKey(autoGenerate = true)
private int id;

@NonNull
private String word;
//..other fields, getters, setters
}

 

 

 

 


 

 

 

 

4. DAO 생성

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#4

 

[DAO 란]

DAO (data access object)는 SQL 쿼리를 명시하고 메소드 호출과 연결해준다. 컴파일러는 SQL을 확인하고 @Insert 같은 일반적인 쿼리에 대한 편리한 어노테이션으로  쿼리를 생성해준다. Room은 DAO를 사용함으로서 우리가 clean API를 만들 수 있게 해준다.

 

DAO는 반드시 인터페이스나 추상클래스여야 한다. (inerface or abstract class)

 

기본적으로, 모든 쿼리는 별도의 스레드에서 실행되어야한다.

 

Room은 코루틴이(courtines) 지원된다.  그래서 쿼리를 suspend 식별자로 어노테이션을 한 다음 코루틴 또는 다른 suspension 함수로부터 호출할 수 있다. (참고: suspend function)

 

 

[DAO 구현]

DAO의 쿼리는 다음과 같다.

  1. 알파벳 순으로 단어들 모두 불러오기(SELECT *)
  2. 단어 삽입(INSERT)
  3. 모든 단어 삭제(DELETE)

다음과 같이 DAO 인터페이스를 생성해주도록 한다.

@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()
}

 

  1. WordDao는 인터페이스다. DAO는 인터페이스나 추상 클래스여야만 한다.
  2. @Dao 어노테이션은 룸이 이게 DAO 클래스라는 것을 식별하게 해준다.
  3. suspend fun insert(word: Word) : 한개의 Word를 suspend function 에 insert 한다는 것을 선언한거다.
  4. @Insert annotation 은 SQL문을 안써도 되게 해주는 특별한 DAO 메소드 어노테이션이다. ( 이 외에도 @Delete 와@Update 같은 행 삭제와 업데이트를 위한 어노테이션이 있다. 이 앱에서는 사용하지 않는다. )
  5. onConflict = OnConflictStrategy.IGNORE: 이 conflict strategy는 이미 리스트에 정확히 같은 단어가 있다면 새로운 단어를 무시한다.더 많은 conflict strategies를 알고 싶다면, documentation에서 자세하게 볼 수 있다. 밑에 사진 참고.
  6. suspend fun deleteAll(): 모든 단어를 삭제(DELETE) 하는 suspend 함수 선언이다.
  7. 다수의 엔티티들을 삭제하는 편의 어노테이션은 없다.(4번과 같은) 그래서 @Query 어노테이션에 쿼리문을 적어준다. 
  8. @Query("DELETE FROM word_table"): @Query는 어노테이션에 문자열 매개 변수로 SQL 쿼리를 제공하므로 복잡한 READ 쿼리 및 기타 작업이 가능하다.
  9. fun getAlphabetizedWords(): List<Word>: 모든 단어들을 가져온다. Word 리스트로 반환해준다.
  10. @Query("SELECT * from word_table ORDER BY word ASC"): 오름 차순으로 모든 단어들을 불러온다. (SELECT)

 

 

[ +참고: conflict strategy 종류]

 

 

Room DAOs 에서 더 자세하게 Room DAO에 대해 알아 볼 수 있다.

 

suspend 관련 설명은 7번에 해놨다.

 

 

 

 


 

 

 

 

5. LiveData 클래스

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#5

 

일반적으로 데이터가 변경될 때 UI에 업데이트된 데이터를 표시하는 등의 몇 가지 작업을 수행하고자 하는 경우 데이터가 바뀌면 반응할 수 있도록 데이터를 관찰해야 한다. 

 

데이터가 저장되는 방식에 따라, 이것은 까다로울 수 있다.  앱의 여러 구성 요소에서 데이터에 대한 변경 사항을 관찰하면 구성요소 사이에 명시적이고 엄격한 dependency paths를 생성할 수 있다. 이것은 무엇보다도 테스트와 디버깅을 어렵게 만든다.

 

데이터 관찰을 위한 Lifecycle 라이브러리 클래스인 LiveData는 이 문제를 해결한다.

method description에 LiveData 타입의 반환 값을 사용하고, 데이터베이스가 업데이트될 때 룸은 LiveData를 업데이트하는 데 필요한 모든 코드를 생성한다.

 

참고: LiveData를 룸과 독립적으로 사용할 경우 데이터 업데이트를 관리해야 한다. LiveData에는 저장된 데이터를 업데이트하기 위해 공개적으로 사용할 수 있는 방법이 없다.

LiveData에 저장된 데이터를 업데이트하려면 LiveData 대신 MutableLiveData를 사용해야 한다. MutableLiveData 클래스는 LiveData 객체의 값을 setValue(T) postValue(T)로 설정할 수 있는 두 가지 공개 방법을 가지고 있다. 일반적으로 MutableLiveData는 ViewModel 내에서 사용되며, 그런 다음 ViewModel은 불변의 LiveData 객체만 관찰자에게 노출시킨다.

  

setValue(T) 와 postValue(T) 의 차이점

setValue => Sets the value. If there are active observers, the value will be dispatched to them.

This method must be called from the main thread. If you need set a value from a background thread, you can use postValue(Object)

 

 

 

WordDao에서 getAlphabetizedWords()의 리턴 타입인 List<Word>를 LiveData 로 감싸준다.

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

 

뒤에서 메인 액티비티의 Observer를 통해 데이터의 변경 내용을 추적할 수 있다.

 

See the LiveData documentation to learn more about other ways of using LiveData, or watch this Architecture Components: LifeData and Lifecycle video.  둘다 봤는데 비디오도 잘 설명해놨으므로 보길 추천한다.

 

 

 

 


 

 

 

 

6. Room database 추가

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#6

 

[룸(Room) 데이터베이스란]

  • 룸은 SQLite 데이터베이스의 상위 레이어에 있는 데이터베이스다.
  • 룸은 SQLiteOpenHelper가 처리하는 평범한 작업들을 처리해준다. 
  • 룸은 DAO를 사용하여 데이터베이스에 쿼리를 실행한다.
  • 기본적으로, UI 성능 저하를 피하기위해서, Room은 메인 스레드에서 쿼리를 실행하지 않는다. 룸 쿼리가 LiveData를 반환하면, 쿼리는 백그라운드 스레드에서 자동으로 비동기로 실행된다.
  • 룸은 SQLite 문의 compile-time 검사를 제공한다.

 

 

[룸(Room) 데이터베이스 구현]

룸 데이터베이스는 RoomDatabase를 상속(extend)하고 추상클래스(abstract)여야 한다. 대게 전체 앱에서 룸 데이터베이스는 하나의 인스턴스만 필요하다. (그래서 싱글톤을 사용한다. 코드참고)

 

룸 데이터베이스를 추가하는 코드는 다음과 같다.

// 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 {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java, 
                        "word_database"
                    ).build()
                INSTANCE = instance
                return instance
            }
        }
   }
}

 

[코드 설명]

  1. 룸 데이터베이스는 abstract 클래스이어야 하고 RoomDatabase를 상속(extend)해야한다.
  2. 클래스를 룸 데이터베이스로 하기 위해 @Database 어노테이션을 붙여준다. 그리고 이 어노테이션에 데이터베이스에 속해있는 엔티티들을 선언하고 버전을 설정한다. 각 엔티티는 데이터베이스에 생성될 테이블에 해당한다. 데이터베이스 마이그레이션은 코드랩 범위를 벗어나기 때문에, 빌드 경고를 피하기 위해 exportSchema를 false로 설정하였다. 실제 앱에서는,  현재의 스키마를 버전 제어 시스템에서 검사 할 수 있게 export해주는데 사용하기 위해 Room을 위한 디렉토리 세팅을 고려해야 한다.  
  3. 각 @Dao에 대해 추상적인(abstract) "getter" 메소드를 생성하여 데이터베이스 DAO들을 제공하게 만들어야한다.
  4. 동시에 여러개의 데이터베이스 인스턴스를 갖는 것을 방지하기 위해, WordRoomDatabase singleton 으로 정의하였다.
  5. getDatabase()는 싱글톤을 반환한다. 이것은 처음 접근했을때 한번만 데이터베이스를 생성할 것이다. Room.databaseBuilder를 사용하여  WordRoomDatabase 클래스의 application context에 RoomDatabase 객체를 생성하고 "word_database"라는 이름을 붙여준다. (이와 같이 Room의 database builder를 사용하여 처음 한번만 데이터베이스를 생성한다.)

RoomDatabase=> provides direct access to the underlying database implementation but you should prefer using Dao classes.

 

 

Note: When you modify the database schema, you'll need to update the version number and define a migration strategy

For a sample, a destroy and re-create strategy can be sufficient. But, for a real app, you must implement a migration strategy. See Understanding migrations with Room.

 

In Android Studio, if you get errors when you paste code or during the build process, select Build >Clean Project. Then select Build > Rebuild Project, and then build again. If you use the provided code, there should be no errors at the points where you are instructed to build the app, but there may be errors in-between.

 

 

 

 


 

 

 

 

 

7. 레포지토리(Repository) 생성

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#7

 

[레포지토리(Repository) 란]

레포지토리 클래스는 여러 data sources에 접근을 추상화한다. 레포지토리는 Architecture Components libraries에 속하지는 않지만 코드 분리 및 아키텍처에 대해 제안된 best practice이다.

레포지토리 클래스는 rest of the application에 대한 데이터 엑세스를 위한 클린한 API를 제공한다.

 

[레포지토리 구현]

WordRepository 클래스

// 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 LiveData will notify the observer when the data has changed.
    val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
 
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

 

 

1. DAO는 전체의 데이터베이스가 아닌 레포지토리 생성자로 전달된다. DAO에 데이터베이스에 대한 읽기/쓰기 메소드들이 모두 포함되어 있기 때문에 DAO에 대한 엑세스만 필요하기 때문이다. 그래서 전체 데이터베이스를 레포지토리에 노출시킬 필요가 없다.

 

2. 단어 리스트는 public property이다. 이것은 룸으로부터 단어 리스트 LiveData를 가짐으로서 초기화된다; 앞선 LIveData 목차에서 LiveData를 반환하는 getAlphabetizedWords()를 정의했기 떄문이다. Room은 분리된 스레드에서 모든 쿼리를 실행한다. 그 다음 관찰되는 LiveData는 데이터가 변경될 때 메인 스레드에 있는 observer에게 알려준다.

 

3. suspend 식별자는 코루틴이나 다른 suspending function에게 불러질 필요가 있다고 컴파일러에게 말한다.

 

[suspend function]

suspend function에 대해 요약하면 다음과 같다.

코루틴 안에서 일반적인 메소드는 호출할 수 없다. 왜냐하면 코루틴의 코드는 중간에 잠시 실행을 멈추거나 다시 실행될 수 있기 때문이다. 그래서 코루틴에서 실행할 수 있는 메소드를 만드려면 suspend라는 키워드를 붙여줘야 한다. 

그리고 suspend 함수 안에서 다른 코루틴을 실행할 수 있다고 한다.

예제는 밑과 같다.

// suspend 키워드가 붙은 함수는 코루틴 안에서 사용할 수 있다.
GlobalScope.launch {
    doSupendFun()
    Log.d(TAG, "Hi")
}

// suspend 키워드가 붙어 정의된 함수는 그 안에 코루틴을 사용할 수 도 있다.
private suspend fun doSupendFun() {
    GlobalScope.launch {
        sleep(2000)
        Log.d(TAG, "Hi2")
    }
}

 

 

 

[더 자세한 참고]

Repositories are meant to mediate between different data sources. In this simple example, you only have one data source, so the Repository doesn't do much. See the BasicSample for a more complex implementation.

 

이 부분은 추후 또 정리를 할 예정이다. 이 프로젝트는 말그대로 간단한 예제이기 때문에 데이터 소스를 하나만 사용하였다. (그래서 레포지토리도 비교적 간단하다.) 더 복잡한 구현은 위 BasicSample 깃허브를 참고하자.!!

 

 

 

 


 

 

 

 

8. 뷰 모델(ViewModel) 생성

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#8

 

[ViewModel 이란]

ViewModel의 역할은 UI에 데이터를 제공하고 구성 변경 사항을 유지하는 것이다. ViewModel은 레포지토리와 UI 사이의 communication center(중간매개) 역할을 한다. ViewModel을 사용하여 프래그먼트들 끼리 데이터를 공유할 수 도 있다.  뷰 모델은 lifecycle library 에 속해있다.

개발자 문서 => ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있다.

 

 

뷰모델에 더 자세히 알고싶은 경우 참고 

=> ViewModel Overview , ViewModels: A Simple Example

 

 

 

[ViewModel 사용 이유]

ViewModel은 구성 변경에서 살아남는 라이프사이클에 민감한 방식으로 앱의 UI 데이터를 갖고있는다. 앱의 UI 데이터를 액티비티 및 프래그먼트 클래스에서 분리하면 다음과 같은 단일 책임 원칙을(single responsibility principle) 더 잘 따를 수 있다: 액티비티와 프래그먼트들은 화면에 데이터를 그리는 역할을 하는 반면, ViewModel은 UI에 필요한 모든 데이터를 보관하고 처리할 수 있다.

 

ViewModel에 대한 설명은 앞서 말한 것 처럼 위 참고 안드로이드 개발자 문서 링크를 보는게 좋다. !! (설명이 매우 잘 되어 있다.)

 

ViewModel에서 변하는(chageable) data나 UI에 사용되는 data는 LiveData를 사용하면 좋다. 밑과 같은 이점이 있다.

  • 데이터에 관찰자를(observer) 배치하고(변경 여부를 polling하는 대신) 데이터가 실제로 변경 됬을 때 UI를 업데이트 해주기만 하면 된다.
  • 레포지토리와 UI가 ViewModel에 의해 완전히 분리된다.
  • ViewModel에서 데이터베이스 호출이 없으므로(이 모든 것은 레포지토리에서 처리됨) 코드를 더 테스트하기 쉽게 만들어준다.

 

[viewModelScope 란]

코틀린에서 모든 코루틴들은 CoroutineScope 안에서 실행된다. 이 scope는 코루틴의 lifetime을 컨트롤한다. 내가 scope의 작업을 중단하면 해당 scope에서 시작된 모든 코루틴들이 취소된다.

AndroidX lifecycle-viewmodel-ktx 라이브러리는 ViewModel의 확장 기능으로 scope에서 작업할 수 있게 해준다.

 

코루틴에 대해 더 자세히 알려면 Using Kotlin Coroutines in your Android App codelab , Easy Coroutines in Android: viewModelScope blogpost 참고 (저도 아직 코루틴 기초만 공부한 상태이므로 추후 자세히 공부할 예정)

 

 

[ViewModel 구현]

// Class extends AndroidViewModel and requires application as a parameter.
class WordViewModel(application: Application) : AndroidViewModel(application) {

    // The ViewModel maintains a reference to the repository to get data.
    private val repository: WordRepository
    // LiveData gives us updated words when they change.
    val allWords: LiveData<List<Word>>

    init {
        // Gets reference to WordDao from WordRoomDatabase to construct
        // the correct WordRepository. 
        val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
        repository = WordRepository(wordsDao)
        allWords = repository.allWords
    }

    /**
     * The implementation of insert() in the database is completely hidden from the UI.
     * Room ensures that you're not doing any long running operations on 
     * the main thread, blocking the UI, so we don't need to handle changing Dispatchers.
     * ViewModels have a coroutine scope based on their lifecycle called 
     * viewModelScope which we can use here.
     */
    fun insert(word: Word) = viewModelScope.launch {
        repository.insert(word)
    }
}

 

  1. WordViewModel을 생성한다. 이것은 AndroidViewModel을 상속하고 Application을 매개변수로 받는다.
  2. 레포지토리 참조를 담고있기 위해 private 멤버 변수로 추가한다. (repository)
  3. 단어 리스트를 캐싱하기 위해 LiveData를 public 멤버 변수로 추가한다
  4. init 블록을 생성하고 WordRoomDatabase로부터 WordDao를 참조한다.
  5. init 블록안에서, WordRoomDatabase를 기반으로 WordRepository를 생성한다. 
  6. init 블록안에서, 레포지토리를 사용하여 allWords 라이브데이터를 초기화한다.
  7. 레포지토리의 insert() 메소드를 호출하는 wrapper insert() 메소드를 생성한다. 이와 같이, insert() 구현은 UI로부터 e캡슐화 된다. 메인스레드를 block 하기 위한  insert를 안하기 위해,  새로운 코루틴을 실행하고 suspend function으로 되있는 레포지토리의 insert를 호출한다. 언급한대로, ViewModel 들은 이 샘플앱에서 사용하는 것처럼 viewModelScopr라 불리는 뷰모델들의 life cycle을 기반으로 코루틴 scope를 가지고있다.

 

 

[추가 참고, 공부자료]

Warning: Don't keep a reference to a context that has a shorter lifecycle than your ViewModel! Examples are:

  • Activity
  • Fragment
  • View

Keeping a reference can cause a memory leak, e.g. the ViewModel has a reference to a destroyed Activity! All these objects can be destroyed by the operative system and recreated when there's a configuration change, and this can happen many times during the lifecycle of a ViewModel.

If you need the application context (which has a lifecycle that lives as long as the application does), use AndroidViewModel, as shown in this codelab.

 

Important: ViewModels don't survive the app's process being killed in the background when the OS needs more resources. For UI data that needs to survive process death due to running out of resources, you can use the Saved State module for ViewModels. Learn more here.

 

To learn more about ViewModel classes, watch the Android Jetpack: ViewModel video.

 

To learn more about coroutines, check out the Coroutines codelab.

 

 

 

 


 

 

 

 

9. XML Layout 추가

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#9

 

Drawable , Floating Button 추가 및 리사이클러뷰에 사용될 아이템을 추가한다. 링크참고

 

 

 

 


 

 

 

 

10. 리사이클러뷰 추가

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#10

 

리사이클러뷰를 만들어준다. 

리사이클러뷰 참고 사이트 :  RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder, and RecyclerView.Adapter

 

 

[리사이클러뷰 어댑터 클래스 생성]

class WordListAdapter internal constructor(
        context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var words = emptyList<Word>() // Cached copy of words

    inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val wordItemView: TextView = itemView.findViewById(R.id.textView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
        return WordViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        val current = words[position]
        holder.wordItemView.text = current.word
    }

    internal fun setWords(words: List<Word>) {
        this.words = words
        notifyDataSetChanged()
    }

    override fun getItemCount() = words.size
}

 

 

[onCreate() - setContentView 밑에 작성]

val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)

 

 

 

 


 

 

 

11. Populate the database

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#11

 

데이베이스에 data가 없다. 두가지 방법으로 데이터를 추가할 수 있다.  데이터베이스가 open 될때 몇개의 data를 추가하고, Words 를 추가하기위해 액티비티를 추가해주는 거다.

 

앱이 시작될 때마다 , 모든 내용을 삭제하고 데이터베이스를 다시 채울려면 RoomDatabase.Callback 을 생성하고 open() 을 오버라이딩 해야한다. 왜냐하면 UI 스레드에서 Room 데이터베이스 작업을 수행 할 수 없으므로 onOpen()은 IIO Dispatcher에서 코루틴을 시작해야한다.

 

Note: If you only want to populate the database the first time the app is launched, you can override the onCreate() method within the RoomDatabase.Callback.

 

코루틴을 시작하려면 CoroutineScope가 필요하다. 코루틴 scope를 매개 변수로 가져 오려면 WordRoomDatabase 클래스의 getDatabase 메소드를 업데이트 해야한다.

 

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

 

scope를 전달하도록 WordViewModel의 초기화 블록에서 데이터베이스 검색 초기화 하는 코드를 업데이트 해야한다.

(Update the database retrieval initializer in the init block of WordViewModel to also pass the scope.)

 

val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()

 

 

WordRoomDatabase에서 RoomDatabase.Callback()의 custom implementation 만들어 생성자 매개 변수로 CoroutineScope를 가져온다. 그런 다음 onOpen 메소드를 재정 의하여 데이터베이스를 populate(채워) 해준다.

다음은 WordRoomDatabase 클래스 내에서 콜백을 만드는 코드이다.

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

    override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
        INSTANCE?.let { database ->
            scope.launch {
                populateDatabase(database.wordDao())
            }
        }
    }

    suspend fun populateDatabase(wordDao: 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!
    }
}

 

 

마지막으로 Room.databaseBuilder()에서 .build ()를 호출하기 직전에 database build sequence에 콜백을 추가한다.

.addCallback(WordDatabaseCallback(scope))

 

 

최종코드는 다음과 같다.

@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 onOpen(db: SupportSQLiteDatabase) {
           super.onOpen(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
        }
   }
}

 

 

 

 

 


 

 

 

 

12. NewWordActivity 추가

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#12

 

액티비티를 추가해주고 관련 xml과 코틀린 코드를 짜주면 된다. 링크참고

 

 

 

 

 


 

 

 

13. 데이터 연결(Connect with the data)

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#13

 

마지막 단계는 사용자가 입력한 새 단어들을 저장하고 Word 데이터베이스의 현재 내용들을 RecyclerView에 보여줌으로서 UI를 데이터베이스에 연결하는 것 이다.

데이터베이스의 현재 내용을 보여줄려면 ViewModel에서 LiveData를 관찰하는 옵저버를 추가해야한다.

데이터가 변경 될 때마다 onChanged() 콜백이 호출되어 어댑터의 setWord () 메소드를 호출하여 어댑터의 캐시 된 데이터를 업데이트하고 표시된 목록을 새로 고침한다.

MainActivity에서 ViewModel에 대한 멤버 변수를 다음과 같이 작성한다.

private lateinit var wordViewModel: WordViewModel

ViewModelProviders를 사용하여 ViewModelActivity와 연관시켜준다.

Activity 처음 시작되면 ViewModelProvidersViewModel을 만든다. 예를 들어 configuration change을 통해 Activity가 destory됬을 때 ViewModel은 지속된다. Activity가 다시 생성되면 ViewModelProviders는 기존 ViewModel을 반환한다. 자세한 내용은 ViewModel을 참조하면 된다.

RecyclerView 코드 블록 아래의 onCreate ()에서 ViewModelProvider에서 ViewModel을 가져온다.

wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)

 

 

또한 onCreate() 에서 WordViewModel의 allWords LiveData property에 대한 옵저버를 추가한다.
관찰 된 데이터가 변경되고 activity가 foreground에있을 때 onChanged () 메소드(람다의 기본 메소드로 되있음)가 실행됩니다.

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
})

 

 

FAB을 누를 때 NewWordActivity를 open하고 MainActivity로 돌아 오면 데이터베이스에 새 단어를 삽입하거나 Toast를 표시하려고 한다. 이를 위해 request code를 정의해보도록 한다.

private val newWordActivityRequestCode = 1

 

 

MainActivity에서 NewWordActivity에 대한 onActivityResult() 코드를 추가한다.

활동이 RESULT_OK와 함께 리턴되면 WordViewModelinsert() 메소드를 호출하여 리턴 된 단어를 데이터베이스에 삽입하십시오.

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_LONG).show()
    }
}

 

 

MainActivity에서 사용자가 FAB을 누를 때 NewWordActivity를 시작한다. MainActivity onCreate에서 FAB에 밑과 같이 onClickListener를 추가한다.

val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
  val intent = Intent(this@MainActivity, NewWordActivity::class.java)
  startActivityForResult(intent, newWordActivityRequestCode)
}

 

 

최종 코드는 다음과 같다.

class MainActivity : AppCompatActivity() {

   private const val newWordActivityRequestCode = 1
   private lateinit var wordViewModel: WordViewModel

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       setSupportActionBar(toolbar)

       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.allWords.observe(this, Observer { words ->
           // Update the cached copy of the words in the adapter.
           words?.let { adapter.setWords(it) }
       })

       val fab = findViewById<FloatingActionButton>(R.id.fab)
       fab.setOnClickListener {
           val intent = Intent(this@MainActivity, NewWordActivity::class.java)
           startActivityForResult(intent, newWordActivityRequestCode)
       }
   }

   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_LONG).show()
       }
   }
}

 

 

 

 


 

 

 

 

14. 요약

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#14

 

이제 앱을 완성했으므로 앱의 구조에 대해 다시 살펴 보겠다.

[앱의 구성요소]

MainActivity : RecyclerView 및 WordListAdapter를 사용하여 단어를 목록에 표시한다. MainActivity에는 데이터베이스에서 LiveData인 Word(단어들)를 관찰하고 변경 될 때 알림을받는 옵저버가(Observer) 있다.


NewWordActivity : 리스트에 새 단어를 추가한다.


WordViewModel : 데이터 계층에 액세스하기위한 메소드를 제공하며 MainActivity가 옵저버 관계를 맺을 수 있도록 LiveData를 반환한다. 


LiveData<List<Word>> : UI Components에서 자동 업데이트가 가능하다. MainActivity에는 데이터베이스에서 LiveData라는 단어를 관찰하고 변경 될 때 알림을받는 옵저버가 있다.


Repository: 하나 이상의 data source를 관리한다. 레포지토리는 ViewModel이 기본 데이터 공급자와(underlying data provider) 상호 작용할 수있는 메서드를 제공한다. 이 앱에서 백엔드는 Room 데이터베이스다.


Room :룸은 래퍼이며(wrapper) SQLite 데이터베이스를 구현한다. Room은 내가 스스로해야했던 많은 일을 대신해준다.

 

DAO : 메서드 호출을 데이터베이스 쿼리에 매핑하므로 레포지토리가 getAlphabetizedWords()와 같은 메서드를 호출 할 때 Room은 SELECT * from word_table ORDER BY word ASC를 실행할 수 있다.


Word: 단일 단어를 포함하는 엔티티 클래스다.

 

* 뷰와 액티비티 (및 프래그먼트)는 ViewModel을 통해서만 데이터와 상호 작용한다. 따라서 데이터의 출처는 중요하지 않다.

 

 

[Flow of Data for Automatic UI Updates (Reactive UI)]

LiveData를 사용하고 있기 때문에 자동 업데이트가 가능하다. MainActivity에는 데이터베이스에서 LiveData라는 단어를 관찰하고 변경 될 때 알림을받는 옵저버가 있다. 변경 사항이 있으면 관찰자의 onChange() 메서드가 실행되고 WordListAdapter에서 mWord가 업데이트 된다.

LiveData이므로 데이터를 관찰할 수 있다. 그리고 관찰되는 것은 WordViewModel allWords property에 의해 반환되는 LiveData<List<Word>>다.

WordViewModel은 백엔드에 대한 모든 것을 UI 계층으로 부터 숨긴다. 이것은 데이터 계층에 액세스하는 방법을 제공하며 MainActivity가 옵저버 관계를 설정할 수 있도록 LiveData를 반환한다. 액티비티(및 프래그먼트)은 ViewModel을 통해서만 데이터와 상호 작용합니다. 따라서 데이터의 출처는 중요하지 않다.

이 경우 데이터는 Repository에서 가져온다. ViewModelRepository와 상호 작용하는 것을 알 필요가 없다. 단지Repository에서 노출되있는 메소드를 통해 Repository와 상호 작용하는 방법만 알면된다.

Repository는 하나 이상의 data source를 관리한다. WordListSample 앱에서 해당 백엔드는 Room데이터베이스다. Room은 래퍼이며(wrapper) SQLite 데이터베이스를 구현한다. Room은 내가 스스로해야했던 많은 일을 대신 해준다. 예를 들어 Room은 SQLiteOpenHelper 클래스와 관련된 모든 작업을 수행한다.

DAO는 메서드 호출을 데이터베이스 쿼리에 매핑하므로 레포지토리가 getAllWords()와 같은 메서드를 호출 할 때 Room은 SELECT * from word_table ORDER BY word ASC를 실행할 수 있다.

쿼리에서 반환 된 결과는 LiveData로 관찰되므로 Room의 데이터가 변경 될 때마다 Observer 인터페이스의 onChanged () 메서드가 실행되고 UI가 업데이트된다.

 

 

 


 

 

15. 더 공부할 자료들

 

The solution code includes unit tests for the Room database. Testing is beyond the scope of this codelab. Take a look at the code if you are interested.

If you need to migrate an app, see 7 Steps to Room after you successfully complete this codelab. Note that it is incredibly satisfying to delete your SQLiteOpenHelper class and a whole lot of other code.

When you have lots of data, consider using the paging library. The paging codelab is here.

More on Architecture, Room, LiveData and ViewModel

Other Architecture Component codelabs

  • Databinding Codelab - Remove even more code from your activities and fragments; works great with ViewModel and LiveData
  • Paging Codelab - Page through huge lists of data from Room
  • Navigation Codelab - Handle in app navigation using the Navigation component and tooling
  • WorkManager Codelab - Efficiently do work when your app is in the background

 

[Optional] Download the solution code

If you haven't already, you can take a look at the solution code for the codelab. You can look at the github repository or download the code here:

Download source code

Unpack the downloaded zip file. This will unpack a root folder, android-room-with-a-view-kotlin, which contains the complete app.

 

 

 

 

이번 아키텍처에서는 데이터바인딩을 사용하지 않았지만, 아키텍처의 흐름을 배울 수 있어 좋았고 복습을 잘 해놔야겠다. 

 

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!

 

 

 

 

728x90
Comments