본문 바로가기
android/android kotlin fundamentals

06-2. Coroutine and Room

by 유저혀 2021. 6. 25.
반응형

Android Kotlin Fundamentals는 codelab에 올라와있는 강의를 한글로 번역한 내용입니다.

 

Codelabs for Android Kotlin Fundamentals  |  Training Courses

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates. Last updated 2020-09-14 UTC.

developer.android.com

코드는 github를 참고해주세요.

 

 

06-2 Coroutines and Room


이제 데이터베이스와 UI가 있으므로 데이터를 수집하고 데이터베이스에 데이터를 추가하고 데이터를 표시해야 한다. 이 모든 작업은 view model에서 진행한다. sleep-tracker view model은 button click들을 handle하면서 DAO를 통해 database와 상호작용하고, LiveData를 통해 UI에 데이터를 제공한다. 모든 데이터베이스 작업은 기본 UI 스레드에서 실행해야하며 코루틴을 사용하여 수행한다.

 

1. Add a ViewModel

Step 1: Add SleepTrackerViewModel

1) sleeptracker 패키지에서 SleepTrackerViewModel.kt 파일을 연다.

2) SleepTrackerViewModel 클래스가 AndroidViewModel()을 상속받은 것을 확인해라. 이 클래스는 ViewModel과 동일하지만 파라미터로 application context를 받아서 매개변수로 사용한다

 

class SleepTrackerViewModel(val database: SleepDatabaseDao, 
	application: Application) : AndroidViewModel(application) {

}

 

 

Step 2: Add SleepTrackerViewModelFactory

1) sleeptracker 패키지에서 SleepTrackerViewModelFactory.kt 파일을 연다

2) factory 코드를 살펴보자

 

class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

 

  • SleepTrackViewModelFactory는 ViewModel과 동일한 인자를 사용하고 ViewModelProvider.Factory를 상속한다
  • factory 안에서 모든 클래스 타입을 인자로 받고 viewModel을 리턴하는 create() 코드를 오버라이드한다
  • create() 메소드의 body 안에서 코드는 사용 가능한 SleepTrackerViewModel 클래스가 있는지 체크하고 있는 경우 인스턴스를 반환하고 그렇지 않으면 예외를 발생시킨다

 

Step 3: Update SleepTrackerFragment

1) SleepTrackerFragment에서 application context의 참조를 가져온다. onCreateView()의 binding 아래에 해당 참조를 넣는다.

  • requireNotNull()은 코틀린 함수로 value가 null일 경우 IllegalArgumentException을 발생시킨다

val application = requireNotNull(this.activity).application

 


2) DAO 레퍼런스를 통해 data source 참조를 얻는 작업이 필요하다. onCreateView() 에서 return 전에 dataSource를 정의한다. 데이터베이스의 DAO 레퍼런스는 SleepDatabase.getInstance(application).sleepDatabaseDao를 통해 얻을 수 있다

 

val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

 


3) onCreate()에서 dataSource와 application을 인자로 넘겨 viewModelFactory 인스턴스를 생성한다

 

val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

 


4) 이제 viewModelFactory를 사용하여 SleepTrackerViewModel 레퍼런스를 얻을 수 있다. 파라미터 SleepTrackerViewModel::class.java는 오브젝트의 런타임 Java 클래스를 나타낸다

 

val sleepTrackerViewModel = ViewModelProvider(
		this, viewModelFactory).get(SleepTrackerViewModel::class.java)

 


5) onCreateview()의 최종 코드는 아래와 같다

 

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)
        val application = requireNotNull(this.activity).application
        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
        val sleepTrackerViewModel =
                ViewModelProvider(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)
        return binding.root
    }

 

 

Step 4: Add data binding for the view model

fragment_sleep_tracker.xml 레이아웃에서

1) <data> 블럭 안에 SleepTrackerViewModel 레퍼런스를 <variable>에 추가한다

 

<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

 

 

SleepTrackerFragment에서

1) 현재 액티비티를 바인딩의 라이프 사이클 소유자로 설정한다. onCreateView() 메소드에 아래 함수를 추가한다.

 

binding.setLifecycleOwner(this)

 


2) sleepTrackerViewModel 바인딩 변수를 sleepTrackerViewModel 객체값으로 지정한다. 아래 코드를 onCreate()의 SleepTrackerViewModel을 생성하는 코드 아래 넣는다.

 

binding.sleepTrackerViewModel = sleepTrackerViewModel

 


3) 바인딩 오브젝트를 다시 생성해야 하므로 오류가 표시 될 수도 있다. 오류를 제거하기 위해 프로젝트를 클린하고 다시 빌드한다.




2. Concept: Coroutines

메인 스레드를 차단하지 않고 장시간의 실행 작업을 수행하는 한 가지 패턴은 콜백을 사용하는 것이다. 코틀린에서 코루틴은 장기 실행 작업을 우아하고 효율적으로 처리하는 방법이다. Kotlin 코루틴을 사용하면 콜백 기반 코드를 순차 코드로 변환할 수 있다. 순차적으로 작성된 코드는 일반적으로 읽기 쉽고 예외와 같은 언어 기능을 사용할 수도 있다. 결국 코루틴과 콜백은 동일한 작업을 수행합니다. 장기 실행 작업에서 결과를 사용할 수 있을 때까지 계속 기다렸다가 실행한다. 

코루틴은 다음 속성을 가지고 있다

  • 코루틴은 비동기이며 non-blocking이다
  • 코루틴은 suspend 함수를 사용하여 비동기 코드를 순차적으로 만든다

코루틴은 비동기이다

  • 코루틴은 프로그램의 main 실행 단계와 독립적으로 실행되며 병렬 또는 별도의 프로세스에서 실행된다
  • 앱의 나머지 부분이 입력을 기다리는 동안 약간의 작업을 따로 처리할 수 있다.
  • 예를 들어 조사가 필요한 질문이 있고 동료에게 답을 찾도록 요청한다고 가정한다면, 그것이 '비동기적으로' 그리고 '별도의 스레드'에서 작업하는 것과 유사하다. 동료가 돌아와서 답변이 무엇인지 알려주기 전 까지 다른 작업을 계속 수행할 수 있다

코루틴은 non bocking이다

  • non blocking의 의미는 코루틴이 main 또는 UI thread를 차단하지 않음을 의미한다.
  • 따라서 코루틴을 사용하면 UI 작업이 항상 우선하므로 사용자에게 부드러운 UI 처리를 제공할 수 있다

코루틴은 suspend 함수를 사용하여 비동기 코드를 순차 코드로 만들 수 있다

  • 코루틴에서 suspend로 표시된 함수를 실행하면, 보통의 함수처럼 함수의 결과가 리턴될 때 까지 blocking 하지 않고 결과가 준비될 때 까지 실행을 일시 중단한다
  • suspend는 모든 로컬 변수를 저장하여 현재 코루틴 실행을 정지한다
  • resume은 정지된 위치부터 정지된 코루틴을 계속 실행합니다
  • 코루틴은 일시 중단되어 결과를 기다리는 동안에 실행 중인 thread는 block 되지 않는다. 그래서 다른 코드나 코루틴이 실행될 수 있다
  • suspend 키워드는 코드가 실행되는 스레드를 지정하지 않는다. suspend 함수는 백그라운드 스레드 또는 메인 스레드에서 실행될 수 있다.
  • blocking과 suspend의 차이점은 스레드는 block되면 다른 작업이 발생하지 않는다는 점이다. 스레드가 suspend된 경우에는 결과를 사용할 수 있을 때 까지 다른 작업을 수행할 수 있다.

코루틴을 코틀린에서 사용하려면 3가지가 필요하다

  • A job
  • A dispatcher
  • A scope

 

Job: 기본적으로 job은 취소할 수 있다. 모든 코루틴은 job을 가지고 있고 코루틴을 취소하기 위해 job을 사용할 수 있다. job은 부모-자식 계층 구조로 배열될 수 있으며 부모 job을 취소하면 일일이 수동으로 취소하지 않아도 모든 자식 job은 알아서 취소된다.

 

Dispatcher: 디스패처는 다양한 스레드에서 실행하기 위해 코루틴을 보낸다.

  • Dispatcher.Main: main에서 코루틴을 시작한다. 이 디스패처는 UI와 상호 작용하고 빠른 작업을 수행하기 위해서만 사용해야 한다. 예를 들어 suspend 함수를 호출하고, Android UI 프레임워크 작업을 실행하고, LiveData 객체를 업데이트한다
  • Dispatcher.IO: 이 디스패처는 기본 스레드 외부에서 디스크 또는 네트워크 I/O를 수행하도록 최적화되어 있다. 예를 들어 파일을 읽거나 네트워킹 작업을 수행한다.
  • Dispatcher.Default: 이 디스패처는 CPU를 많이 사용하는 작업을 기본 스레드 외부에서 수행하도록 최적화되어 있다. 목록을 정렬하고 JSON을 파싱하는 등의 작업을 수행한다

Scope: 코루틴의 scope는 코루틴이 실행되는 context의 범위를 정의한다. scope는 코루틴의 job과 Dispatcher에 대한 정보를 결합한다. scope는 코루틴을 추적한다. 코루틴을 시작하면 scope 안에 있는데, 즉 코루틴이 추적할 scope를 나타낸다.



Kotlin coroutines with Architecture components

CoroutineScope는 모든 코루틴을 추적하고 코루틴이 실행되어야하는시기를 관리하는 데 도움이 된다. 또한 그 안에서 시작된 모든 코루틴을 취소 할 수 있습니다. 각 비동기 작업 또는 코루틴은 특정 CoroutineScope 내에서 실행된다.

 

Architecture components는 앱의 논리적 범위에 대한 코루틴에 대한 최고 수준의 지원을 제공한다. 아키텍처 구성 요소는 앱에서 사용할 수 있는 다음과 같은 기본 제공 범위를 정의한다. built-in 코루틴 범위는 각 해당 아키텍처 구성 요소에 대한 KTX 확장에 있다. 이러한 범위를 사용할 때 적절한 dependency를 추가해야 한다.

ViewModelScope: ViewModelScope는 앱의 각 ViewModel에 대해 정의된다. 이 범위에서 시작된 모든 코루틴은 ViewModel이 지워지면 자동으로 취소된다. 이 코드 랩에서는 ViewModelScope를 사용하여 데이터베이스 작업을 시작한다.

 

 

Room and Dispatcher

Room 라이브러리를 사용하여 데이터베이스 작업을 수행 할 때 Room은 Dispatchers.IO를 사용하여 백그라운드 스레드에서 데이터베이스 작업을 수행한다. Dispatcher를 명시적으로 지정할 필요는 없다. Room에서 이 작업을 수행한다.

 

 


3. Collect and display the data

다음과 같은 방식으로 사용자가 sleep data와 상호작용 하기를 원한다

  • 사용자가 start 버튼을 누르면 앱은 새로운 sleep night를 생성하고 데이터베이스에 sleep night을 저장한다
  • 사용자가 stop 버튼을 누르면 앱은 sleep night의 end time을 갱신한다
  • 사용자가 clear 버튼을 누르면 데이터베이스에 있는 데이터를 지운다. 이러한 데이터베이스 작업은 오래 걸릴 수 있으므로 별도의 스레드에서 실행해야 한다

 

Step 1: DAO에 suspend 함수 추가

1) database/SleepDatabaseDao.kt 파일을 열고 getAllNights() 제외한 메소드들에 suspend 키워드를 붙인다. 왜냐하면 Room이 이미 LiveData를 반환하는 특정 @Query에 대한 백그라운드 스레드를 사용하기 때문에 LiveData를 리턴하는 getAllNight() 메소드에는 따로 suspend를 추가하지 않아도 된다.

 

@Dao
interface SleepDatabaseDao {

   @Insert
   suspend fun insert(night: SleepNight)

   @Update
   suspend fun update(night: SleepNight)

   @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
   suspend fun get(key: Long): SleepNight?

   @Query("DELETE FROM daily_sleep_quality_table")
   suspend fun clear()

   @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
   suspend fun getTonight(): SleepNight?

   @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
   fun getAllNights(): LiveData<List<SleepNight>>
}

 

 

Step 2: 데이터베이스 작업을 위한 코루틴 설정

Sleep Tracker 앱의 시작 버튼을 누르면 SleepTrackerViewModel에서 함수를 호출하여 SleepNight의 새 인스턴스를 만들고 데이터베이스에 인스턴스를 저장하려고 한다. 버튼을 누르면 SleepNight 생성 또는 업데이트와 같은 데이터베이스 작업이 트리거된다. 데이터베이스 작업은 시간이 걸릴 수 있으므로 코루틴을 사용하여 앱 버튼의 클릭 핸들러를 구현한다.

 

1) app-level의 build.gradle 파일을 아래와 같은 디펜던시를 추가한다.

 

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"

 


2) SleepTrackerViewModel 파일을 연다.

 

3) current night를 가지고 있는 tonight 변수를 정의한다. 이 변수를 변경하고 변화를 observe하기 위해 MutableLiveData로 만든다.

 

private var tonight = MutableLiveData<SleepNight?>()

 

 

4) tonight 변수를 초기화하기 위해서는 가능한한 init 블럭을 만들고 initializeTonight() 함수를 호출한다. initializeTonight()은 이후에 정의한다.

 

init {
   initializeTonight()
}

 

 

5) init 블럭 아래에 initializeTonight()을 구현한다. viewModelScope 범위 내에서 코루틴을 시작하기 위해 ViewModelScope.launch를 사용한다. getTonightFromDatabase() 함수를 호출하여 데이터베이스로부터 tonight 값을 얻어오고 tonight.value에 할당한다. launch에서 {} 중괄호를 사용한 것을 보면 람다식을 사용한 것을 알 수 있다. 

 

private fun initializeTonight() {
   viewModelScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}

 

 

6) 이번 단계에서는 getTonightFromDatabase()를 구현한다. private suspend를 붙이고 nullable한 SleepNight를 리턴하는 함수를 만든다. 

 

private suspend fun getTonightFromDatabase(): SleepNight? { }

 

 

7) getTonightFromDatabase() 함수 본문에 database로부터 tonight을 get하는 코드를 추가한다. start와 endtime이 같지 않은 경우 이미 그 night는 완료됐음을 의미하므로 null을 리턴하고 그렇지 않은 경우 night를 리턴한다. 

 

var night = database.getTonight()
if (night?.endTimeMilli != night?.startTimeMilli) {
  night = null
}
return night

 

 

8) suspend 키워드를 추가한 getTonightFromDatabase() 함수는 아래와 같다.

 

private suspend fun getTonightFromDatabase(): SleepNight? {
    var night = database.getTonight()
    if (night?.endTimeMilli != night?.startTimeMilli) {
        night = null
    }
    return night
}

 

 

 

Step 3: Add the click handler for the Start button

이제 start button의 클릭 핸들러인 onStartTracking()을 구현해보자. 새로운 SleepNight을 생성하여 데이터베이스에 저장한 다음 tonight에 할당해야 한다. onStartTracking()는 initializeTonight()과 비슷해질 것이다


1) SleepTrackerViewModel.kt 파일에서 getTonightFromDatabase() 함수 아래에 onStartTracking()을 선언한다

 

fun onStartTracking() {}



2) onStartTracking() 내부에서 viewModelScope 안에서 코루틴을 실행한다. 왜냐하면 계속해서 변경되는 result를 UI에 업데이트 하고싶기 때문이다.

 

viewModelScope.launch {}

 

 


3) coroutine launch 블럭 안에 현재시간을 시작 시간으로 가지고 있는 새로운 SleepNight() 객체를 만든다.

 

val newNight = SleepNight()

 


4) coroutine launch 안에서 insert()를 호출하여 데이터베이스에 newNight을 추가한다. 지금은 insert() 메소드가 suspend로 정의되어 있지 않으므로 에러가 날 것이다. 이 insert()는 SleepDatabaseDAO.kt에 있는 insert와 다른 메소드임을 주의하라.

 

insert(newNight)

 


5) coroutine launch 코드 안에서 tonight 값을 가져와서 업데이트한다.

 

tonight.value = getTonightFromDatabase()

 


6) onStartTracking() 아래에 SleepNight을 인자로 갖는 private suspend insert() 함수를 정의한다

 

private suspend fun insert(night: SleepNight) {}

 


7) insert() 함수 내에서 database에 night를 insert하기 위해 DAO를 사용한다

 

database.insert(night)

 

 

Room과 함께 코루틴을 쓰면 Dispatchers.IO를 사용한다. 그러므로 main thread에서 실행되지 않는다.


8) fragment_sleep_tracker.xml 레이아웃 파일에서 앞에서 설정한 데이터 바인딩을 사용하여 start_button에 클릭 핸들러로 onStartTracking()를 추가한다

 

android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"

 

 

※ 중요

코루틴의 결과가 UI에 표시되는 내용에 영향을 미치므로 main UI Thread에서 실행되는 coroutine을 실행한다. 다음 예제와 같이 viewModelScope 속성을 통해 viewModel의 CoroutineScope에 액세스 할 수 있다.

long-running 작업을 위해서는 result를 기다리는 동안 UI thread를 block하지 않기 위해 suspend 함수를 호출한다. long-running 작업은 UI에 영향을 미칠 수는 있지만 해당 작업은 UI와는 독립적이다. 효율성을 위해 이 작업을 I/O dispatcher로 전환한다. (Room에서 코드 생성)

I/O 디스패처는 최적화된 스레드 풀을 사용하고 이러한 종류의 작업을 위해 따로 설정한다. 그런 다음 장기 실행 함수를 호출하여 작업을 수행한다. 패턴은 아래와 같다.

 

Without Room

 

fun someWorkNeedsToBeDone {
   viewModelScope.launch {
        suspendFunction()
   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

 

 

With Room

 

// Using Room
fun someWorkNeedsToBeDone {
   viewModelScope.launch {
        suspendDAOFunction()
   }
}

suspend fun suspendDAOFunction() {
   // No need to specify the Dispatcher, Room uses Dispatchers.IO.
   longrunningDatabaseWork()
}

 

 

 

Step 4: Display the data

  • DAO의 getAllNights ()가 LiveData를 반환하므로 SleepTrackerViewModel에서 nights 변수는 LiveData를 참조한다.
  • 데이터베이스의 데이터가 변경될 때 마다 LiveData nights가 최신 데이터를 표시하도록 업데이트 되는게 Room 기능 중 하나이다. Room은 데이터베이스와 일치하도록 데이터를 업데이트 하므로 LiveData를 명시적으로 set하거나 update할 필요가 없다
  • 텍스트 뷰에 night를 표시하면 객체 참조값이 표시되는데, 객체의 내용을 보기 위해서는 형식화된 문자열로 변환해야 한다. 데이터베이스에서 새로운 데이터를 가져올 때 transformation map을 실행시켜 보자

 

1) Util.kt 파일을 열어 formatNight() 메소드 주석을 해제하고 import를 추가한다

 

2) formatNights()의 리턴 타입이 HTML 형식 문자열인 Spanned 인 것을 확인한다

 

3) strings.xml을 열어서 CDATA를 사용하여 sleep data를 표시하기 위해 문자열 리소스 포맷을 사용한다

 

4) SleepTrackerViewModel 파일을 열어서 nights라는 변수를 정의한다. nights에는 데이터베이스에서 모든 nights의 값을 가져와 할당시킨다.

 

private val nights = database.getAllNights()

 


5) nights 선언 바로 아래에 nights를 nightsString으로 변환하는 코드를 넣는다. Util.kt의 foramtNights() 함수를 사용한다. nights를 Transformations 클래스의 map() 함수에 전달한다. 문자열 리소스에 액세스 하기 위해 nights와 Resource를 매개변수로 하는 formatNights() 함수를 호출한다

 

val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}

 


6) fragment_sleep_tracker.xml 레이아웃을 열어서 TextView에 anroid:text 속성을 추가하여 nightsString에 대한 참조로 바꿀 수 있다

 

"@{sleepTrackerViewModel.nightsString}"

 


7) 앱을 빌드하고 실행시킨다. 모든 sleep data의 start time이 화면에 나타난다. start button을 한번 더 눌러서 데이터가 추가되는지 확인해본다. 다음 단계에서는 Stop 버튼을 구현한다

 

Step 5: Add the click handler for the Stop button

이전 단계에서 진행했던 같은 패턴을 사용하여 SleepTrackerViewModel의 Stop button 클릭 핸들러를 구현한다

 

1) viewModel에 onStopTracking()을 추가하고, 코루틴을 viewModelScope에서 실행시킨다. end time이 아직 저장되지 않았다면 endTimeMilli에 현재 시간을 넣고 night data로 update()를 호출한다

  • 코틀린에서는 return@label 문법을 이용해서 여러 중첩 함수 중 이 명령이 어떤 함수를 리턴하는지 지정할 수 있다

 

fun onStopTracking() {
   viewModelScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}

 


2) insert()를 구현했던 같은 패턴을 사용하여 update()를 구현한다

 

private suspend fun update(night: SleepNight) {
    database.update(night)
}

 


3) fragment_sleep_tracker.xml 파일을 열어서 stop_button에 클릭 핸들러를 추가한다

 

android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"

 


4) 앱을 빌드시키고 실행시켜서 Start 버튼을 누르고 Stop 버튼을 누른다.

 

Step 6: Add the click handler for the Stop button

1) 유사하게 onClear()와 clear()를 구현한다

 

fun onClear() {
   viewModelScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
    database.clear()
}

 


2) fragment_sleep_tracker.xml을 열어서 clear_button 버튼에 클릭 핸들러를 연결한다

 

android:onClick="@{() -> sleepTrackerViewModel.onClear()}"

 


3) 앱을 실행시켜서 Clear를 눌러 모든 데이터를 제거한다

728x90

'android > android kotlin fundamentals' 카테고리의 다른 글

07-1. RecyclerView  (0) 2021.06.28
06-3. Use LiveData to control button states  (0) 2021.06.26
06-1. Room Database  (0) 2021.02.21
05-4. LiveData Transformations  (0) 2021.02.21
05-3. Databinding with ViewModel and LiveData  (0) 2021.02.18

댓글