Android Kotlin 기초 06.2: 코루틴 및 Room

이 Codelab은 Android Kotlin 기초 과정의 일부입니다. Codelab을 순서대로 진행하면 이 과정의 학습 효과를 극대화할 수 있습니다. 모든 과정 Codelab은 Android Kotlin 기본사항 Codelab 방문 페이지에 나열되어 있습니다.

소개

앱의 완벽한 사용자 환경을 만드는 데 있어 최우선 순위 중 하나는 UI가 항상 응답성이 있고 원활하게 실행되도록 하는 것입니다. UI 성능을 개선하는 한 가지 방법은 데이터베이스 작업과 같은 장기 실행 작업을 백그라운드로 이동하는 것입니다.

이 Codelab에서는 Kotlin 코루틴을 사용하여 기본 스레드에서 데이터베이스 작업을 실행하여 TrackMySleepQuality 앱의 사용자 대상 부분을 구현합니다.

기본 요건

다음을 잘 알고 있어야 합니다.

  • 활동, 프래그먼트, 뷰, 클릭 핸들러를 사용하여 기본 사용자 인터페이스 (UI)를 빌드합니다.
  • 프래그먼트 간 탐색 및 safeArgs를 사용하여 프래그먼트 간에 간단한 데이터 전달
  • 모델, 모델 팩토리, 변환, LiveData를 확인합니다.
  • Room 데이터베이스를 만들고, DAO를 만들고, 항목을 정의하는 방법
  • 스레딩 및 멀티프로세싱 개념에 익숙하면 도움이 됩니다.

학습할 내용

  • Android에서 스레드가 작동하는 방식
  • Kotlin 코루틴을 사용하여 데이터베이스 작업을 기본 스레드에서 떨어뜨려 놓는 방법
  • TextView에 형식이 지정된 데이터를 표시하는 방법

실습할 내용

  • TrackMySleepQuality 앱을 확장하여 데이터베이스에서 데이터를 수집, 저장, 표시합니다.
  • 코루틴을 사용하여 백그라운드에서 장기 실행 데이터베이스 작업을 실행합니다.
  • LiveData를 사용하여 탐색과 스낵바 표시를 트리거합니다.
  • LiveData를 사용하여 버튼을 사용 설정 및 중지합니다.

이 Codelab에서는 TrackMySleepQuality 앱의 뷰 모델, 코루틴, 데이터 표시 부분을 빌드합니다.

앱에는 프래그먼트로 표현되는 두 개의 화면이 있습니다(아래 그림 참고).

왼쪽에 표시된 첫 번째 화면에는 추적을 시작하고 중지하는 버튼이 있습니다. 화면에는 사용자의 모든 수면 데이터가 표시됩니다. 지우기 버튼은 앱이 사용자를 위해 수집한 모든 데이터를 완전히 삭제합니다.

오른쪽에 표시된 두 번째 화면은 수면의 질 평가를 선택하는 화면입니다. 앱에서 평점은 숫자로 표시됩니다. 개발 목적으로 앱은 얼굴 아이콘과 숫자 값을 모두 표시합니다.

사용자의 흐름은 다음과 같습니다.

  • 사용자가 앱을 열면 수면 추적 화면이 표시됩니다.
  • 사용자가 시작 버튼을 탭합니다. 시작 시간을 기록하고 표시합니다. 시작 버튼은 사용 중지되고 중지 버튼은 사용 설정됩니다.
  • 사용자가 중지 버튼을 탭합니다. 이렇게 하면 종료 시간이 기록되고 수면의 질 화면이 열립니다.
  • 사용자가 수면의 질 아이콘을 선택합니다. 화면이 닫히고 추적 화면에 수면 종료 시간과 수면의 질이 표시됩니다. 중지 버튼은 사용 중지되고 시작 버튼은 사용 설정됩니다. 앱이 또 다른 밤을 맞이할 준비가 되었습니다.
  • 데이터베이스에 데이터가 있으면 언제든지 지우기 버튼이 사용 설정됩니다. 사용자가 지우기 버튼을 탭하면 모든 데이터가 복구할 수 없이 삭제됩니다. '정말로 지우시겠습니까?' 메시지가 표시되지 않습니다.

이 앱은 전체 아키텍처의 컨텍스트에서 아래와 같이 간소화된 아키텍처를 사용합니다. 앱은 다음 구성요소만 사용합니다.

  • UI 컨트롤러
  • 모델 및 LiveData 보기
  • Room 데이터베이스

이 작업에서는 TextView를 사용하여 서식이 지정된 수면 추적 데이터를 표시합니다. (이것은 최종 인터페이스가 아닙니다. 다른 Codelab에서 더 나은 방법을 알아봅니다.)

이전 Codelab에서 빌드한 TrackMySleepQuality 앱을 계속 사용하거나 이 Codelab의 시작 앱을 다운로드하면 됩니다.

1단계: 시작 앱 다운로드 및 실행

  1. GitHub에서 TrackMySleepQuality-Coroutines-Starter 앱을 다운로드합니다.
  2. 앱을 빌드하고 실행합니다. 앱에 SleepTrackerFragment 프래그먼트의 UI가 표시되지만 데이터는 표시되지 않습니다. 버튼을 눌러도 반응하지 않습니다.

2단계: 코드 검사

이 Codelab의 시작 코드는 6.1. Room 데이터베이스 만들기 Codelab의 솔루션 코드와 같습니다.

  1. res/layout/activity_main.xml 이 레이아웃에는 nav_host_fragment 프래그먼트가 포함되어 있습니다. <merge> 태그도 확인하세요.

    merge 태그를 사용하면 레이아웃을 포함할 때 중복 레이아웃을 제거할 수 있으므로 사용하는 것이 좋습니다. 중복 레이아웃의 예로는 ConstraintLayout > LinearLayout > TextView가 있습니다. 여기서 시스템은 LinearLayout을 삭제할 수 있습니다. 이러한 최적화를 통해 뷰 계층 구조를 간소화하고 앱 성능을 개선할 수 있습니다.
  2. navigation 폴더에서 navigation.xml을 엽니다. 두 프래그먼트와 이를 연결하는 탐색 작업이 표시됩니다.
  3. layout 폴더에서 수면 추적기 프래그먼트를 더블클릭하여 XML 레이아웃을 확인합니다. 다음 사항에 유의하세요.
  • 데이터 결합을 사용 설정하기 위해 레이아웃 데이터가 <layout> 요소로 래핑됩니다.
  • ConstraintLayout 및 기타 뷰는 <layout> 요소 내에 배치됩니다.
  • 파일에 자리표시자 <data> 태그가 있습니다.

스타터 앱은 UI의 크기, 색상, 스타일도 제공합니다. 앱에는 Room 데이터베이스, DAO, SleepNight 항목이 포함되어 있습니다. 이전 Codelab을 완료하지 않은 경우 코드의 이러한 측면을 직접 살펴보세요.

이제 데이터베이스와 UI가 있으므로 데이터를 수집하고, 데이터베이스에 데이터를 추가하고, 데이터를 표시해야 합니다. 이 모든 작업은 뷰 모델에서 실행됩니다. 수면 추적기 뷰 모델은 버튼 클릭을 처리하고, DAO를 통해 데이터베이스와 상호작용하며, LiveData을 통해 UI에 데이터를 제공합니다. 모든 데이터베이스 작업은 기본 UI 스레드에서 벗어나 실행되어야 하고 코루틴을 사용하면 됩니다.

1단계: SleepTrackerViewModel 추가

  1. sleeptracker 패키지에서 SleepTrackerViewModel.kt를 엽니다.
  2. 시작 앱에 제공되고 아래에도 표시된 SleepTrackerViewModel 클래스를 검사합니다. 클래스가 AndroidViewModel()를 확장합니다. 이 클래스는 ViewModel와 동일하지만 애플리케이션 컨텍스트를 매개변수로 가져와 속성으로 사용할 수 있도록 합니다. 이 값은 나중에 필요합니다.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

2단계: SleepTrackerViewModelFactory 추가

  1. sleeptracker 패키지에서 SleepTrackerViewModelFactory.kt를 엽니다.
  2. 아래에 표시된 팩토리에 제공된 코드를 살펴봅니다.
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")
   }
}

다음 사항에 유의합니다.

  • 제공된 SleepTrackerViewModelFactoryViewModel과 동일한 인수를 사용하고 ViewModelProvider.Factory을 확장합니다.
  • 팩토리 내에서 코드는 클래스 유형을 인수로 사용하고 ViewModel를 반환하는 create()를 재정의합니다.
  • create() 본문에서 코드는 사용 가능한 SleepTrackerViewModel 클래스가 있는지 확인하고, 있는 경우 인스턴스를 반환합니다. 그렇지 않으면 코드에서 예외가 발생합니다.

3단계: SleepTrackerFragment 업데이트

  1. SleepTrackerFragment에서 애플리케이션 컨텍스트에 대한 참조를 가져옵니다. 참조를 binding 아래의 onCreateView()에 넣습니다. 이 프래그먼트가 연결된 앱을 참조하여 뷰 모델 팩토리 제공자에 전달해야 합니다.

    requireNotNull Kotlin 함수는 valuenull인 경우 IllegalArgumentException를 발생시킵니다.
val application = requireNotNull(this.activity).application
  1. DAO 참조를 통해 데이터 소스를 참조해야 합니다. onCreateView()에서 return 앞에 dataSource를 정의합니다. 데이터베이스의 DAO에 대한 참조를 가져오려면 SleepDatabase.getInstance(application).sleepDatabaseDao을 사용합니다.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. onCreateView()에서 return 앞에 viewModelFactory의 인스턴스를 만듭니다. dataSourceapplication을 전달해야 합니다.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. 이제 팩토리가 있으므로 SleepTrackerViewModel 참조를 가져옵니다. SleepTrackerViewModel::class.java 매개변수는 이 객체의 런타임 Java 클래스를 참조합니다.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. 완성된 코드는 다음과 같이 표시됩니다.
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

지금까지의 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 =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

4단계: 뷰 모델의 데이터 결합 추가

기본 ViewModel가 준비되었으므로 SleepTrackerFragment에서 데이터 결합 설정을 완료하여 ViewModel를 UI와 연결해야 합니다.


fragment_sleep_tracker.xml 레이아웃 파일에서 다음을 실행합니다.

  1. <data> 블록 내에서 SleepTrackerViewModel 클래스를 참조하는 <variable>을 만듭니다.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

SleepTrackerFragment에서:

  1. 현재 활동을 바인딩의 수명 주기 소유자로 설정합니다. onCreateView() 메서드 내에서 return 문 앞에 다음 코드를 추가합니다.
binding.setLifecycleOwner(this)
  1. sleepTrackerViewModel 결합 변수를 sleepTrackerViewModel에 할당합니다. SleepTrackerViewModel을 만드는 코드 아래의 onCreateView()에 다음 코드를 넣습니다.
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. 바인딩 객체를 다시 만들어야 하므로 오류가 표시될 수 있습니다. 프로젝트를 정리하고 다시 빌드하여 오류를 없앱니다.
  2. 마지막으로 언제나처럼 코드가 오류 없이 빌드되고 실행되는지 확인합니다.

Kotlin에서 코루틴은 장기 실행 작업을 원활하고 효율적으로 처리하는 방법입니다. Kotlin 코루틴을 사용하면 콜백 기반 코드를 순차 코드로 변환할 수 있습니다. 순차적으로 작성된 코드는 일반적으로 읽기가 더 쉬우며 예외와 같은 언어 기능을 사용할 수도 있습니다. 결국 코루틴과 콜백은 동일한 작업을 합니다. 즉, 장기 실행 작업에서 결과를 사용할 수 있을 때까지 기다렸다가 실행을 계속합니다.

코루틴에는 다음과 같은 속성이 있습니다.

  • 코루틴은 비동기이며 비차단 형태이며
  • 코루틴은 suspend 함수를 사용하여 비동기 코드를 순차 코드로 만듭니다.

코루틴은 비동기입니다.

코루틴은 프로그램의 기본 실행 단계와 독립적으로 실행됩니다. 이는 병렬로 또는 별도의 프로세서에서 실행될 수 있습니다. 앱의 나머지 부분이 입력을 기다리는 동안 약간의 처리를 몰래 수행할 수도 있습니다. 비동기의 중요한 측면 중 하나는 결과를 명시적으로 기다릴 때까지 결과를 사용할 수 있다고 예상할 수 없다는 것입니다.

예를 들어 조사가 필요한 질문이 있고 동료에게 답을 찾아 달라고 요청한다고 가정해 보겠습니다. 이후에 가서 작업을 수행하는 것은 '비동기식'으로 '별도의 스레드에서' 작업을 수행하는 것과 같습니다. 동료가 돌아와서 답을 알려줄 때까지 답과 관련이 없는 다른 작업을 계속할 수 있습니다.

코루틴은 비차단입니다.

차단되지 않음 은 코루틴이 기본 스레드나 UI 스레드를 차단하지 않음을 의미합니다. 따라서 코루틴을 사용하면 UI 상호작용이 항상 우선순위를 가지므로 사용자는 항상 가장 원활한 환경을 경험할 수 있습니다.

코루틴은 정지 함수를 사용하여 비동기 코드를 순차 코드로 만듭니다.

suspend 키워드는 함수 또는 함수 유형을 코루틴에서 사용할 수 있는 것으로 표시하는 Kotlin의 방법입니다. 코루틴이 suspend로 표시된 함수를 호출하면 일반 함수 호출처럼 함수가 반환될 때까지 차단되는 대신 결과가 준비될 때까지 코루틴 실행이 정지됩니다. 그러면 코루틴이 결과와 함께 중단된 지점에서 다시 시작됩니다.

코루틴이 정지되어 결과를 기다리는 동안 코루틴이 실행되는 스레드의 차단이 해제됩니다. 이렇게 하면 다른 함수나 코루틴이 실행될 수 있습니다.

suspend 키워드는 코드가 실행되는 스레드를 지정하지 않습니다. 정지 함수는 백그라운드 스레드 또는 기본 스레드에서 실행될 수 있습니다.

Kotlin에서 코루틴을 사용하려면 다음 세 가지가 필요합니다.

  • 작업
  • 디스패처
  • 범위

작업: 기본적으로 작업은 취소할 수 있는 모든 것입니다. 모든 코루틴에는 작업이 있으며 작업을 사용하여 코루틴을 취소할 수 있습니다. 작업은 상위-하위 계층 구조로 정렬할 수 있습니다. 상위 작업을 취소하면 작업의 모든 하위 항목이 즉시 취소되므로 각 코루틴을 수동으로 취소하는 것보다 훨씬 편리합니다.

디스패처: 디스패처는 다양한 스레드에서 실행되도록 코루틴을 전송합니다. 예를 들어 Dispatcher.Main는 기본 스레드에서 작업을 실행하고 Dispatcher.IO는 차단 I/O 작업을 공유 스레드 풀로 오프로드합니다.

범위: 코루틴의 범위는 코루틴이 실행되는 컨텍스트를 정의합니다. 스코프는 코루틴의 작업 및 디스패처에 관한 정보를 결합합니다. 범위는 코루틴을 추적합니다. 코루틴을 실행하면 '범위 내'에 있게 됩니다. 즉, 코루틴을 추적할 범위를 지정한 것입니다.

사용자가 다음과 같은 방식으로 수면 데이터와 상호작용할 수 있도록 하려고 합니다.

  • 사용자가 시작 버튼을 탭하면 앱에서 새로운 수면 밤을 만들고 수면 밤을 데이터베이스에 저장합니다.
  • 사용자가 중지 버튼을 탭하면 앱이 종료 시간으로 밤을 업데이트합니다.
  • 사용자가 지우기 버튼을 탭하면 앱이 데이터베이스의 데이터를 삭제합니다.

이러한 데이터베이스 작업은 시간이 오래 걸릴 수 있으므로 별도의 스레드에서 실행해야 합니다.

1단계: 데이터베이스 작업을 위한 코루틴 설정

Sleep Tracker 앱의 Start 버튼을 탭하면 SleepTrackerViewModel에서 함수를 호출하여 SleepNight의 새 인스턴스를 만들고 데이터베이스에 인스턴스를 저장해야 합니다.

버튼을 탭하면 SleepNight 생성 또는 업데이트와 같은 데이터베이스 작업이 트리거됩니다. 이러한 이유로 코루틴을 사용하여 앱 버튼의 클릭 핸들러를 구현합니다.

  1. 앱 수준 build.gradle 파일을 열고 코루틴의 종속 항목을 찾습니다. 코루틴을 사용하려면 다음 종속 항목이 필요하며, 이는 자동으로 추가되었습니다.

    $coroutine_version은 프로젝트 build.gradle 파일에 coroutine_version = '1.0.0'로 정의되어 있습니다.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. SleepTrackerViewModel 파일을 엽니다.
  2. 클래스의 본문에서 viewModelJob을 정의하고 Job의 인스턴스를 할당합니다. 이 viewModelJob을 사용하면 뷰 모델이 더 이상 사용되지 않고 소멸될 때 이 뷰 모델에서 시작된 모든 코루틴을 취소할 수 있습니다. 이렇게 하면 반환할 곳이 없는 코루틴이 발생하지 않습니다.
private var viewModelJob = Job()
  1. 클래스 본문 끝에서 onCleared()를 재정의하고 모든 코루틴을 취소합니다. ViewModel가 소멸되면 onCleared()가 호출됩니다.
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. viewModelJob 정의 바로 아래에 코루틴의 uiScope을 정의합니다. 스코프는 코루틴이 실행될 스레드를 결정하며 스코프는 작업에 대해서도 알아야 합니다. 범위를 가져오려면 CoroutineScope 인스턴스를 요청하고 디스패처와 작업을 전달합니다.

Dispatchers.Main를 사용하면 uiScope에서 실행된 코루틴이 기본 스레드에서 실행됩니다. 이는 ViewModel로 시작된 많은 코루틴에 적합합니다. 이러한 코루틴은 일부 처리를 실행한 후 UI를 업데이트하기 때문입니다.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. uiScope 정의 아래에 현재 밤을 저장할 tonight이라는 변수를 정의합니다. 데이터를 관찰하고 변경할 수 있어야 하므로 변수를 MutableLiveData로 만듭니다.
private var tonight = MutableLiveData<SleepNight?>()
  1. tonight 변수를 최대한 빨리 초기화하려면 tonight 정의 아래에 init 블록을 만들고 initializeTonight()를 호출합니다. 다음 단계에서 initializeTonight()를 정의합니다.
init {
   initializeTonight()
}
  1. init 블록 아래에 initializeTonight()를 구현합니다. uiScope에서 코루틴을 실행합니다. 내부에서 getTonightFromDatabase()를 호출하여 데이터베이스에서 tonight 값을 가져오고 tonight.value에 값을 할당합니다. 다음 단계에서 getTonightFromDatabase()를 정의합니다.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. getTonightFromDatabase()를 구현합니다. 현재 시작된 SleepNight이 없는 경우 null 허용 SleepNight을 반환하는 private suspend 함수로 정의합니다. 함수는 무언가를 반환해야 하므로 오류가 발생합니다.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. getTonightFromDatabase()의 함수 본문 내에서 Dispatchers.IO 컨텍스트에서 실행되는 코루틴의 결과를 반환합니다. 데이터베이스에서 데이터를 가져오는 것은 I/O 작업이며 UI와는 관련이 없으므로 I/O 디스패처를 사용합니다.
  return withContext(Dispatchers.IO) {}
  1. return 블록 내에서 코루틴이 데이터베이스에서 tonight (가장 최근 밤)을 가져오도록 합니다. 시작 시간과 종료 시간이 동일하지 않으면 밤이 이미 완료되었음을 의미하므로 null를 반환합니다. 그렇지 않으면 밤을 반환합니다.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

완성된 getTonightFromDatabase() 중단 함수는 다음과 같습니다. 더 이상 오류가 표시되지 않습니다.

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

2단계: 시작 버튼의 클릭 핸들러 추가

이제 Start 버튼의 클릭 핸들러인 onStartTracking()를 구현할 수 있습니다. 새 SleepNight를 만들어 데이터베이스에 삽입하고 tonight에 할당해야 합니다. onStartTracking()의 구조는 initializeTonight()와 매우 유사합니다.

  1. onStartTracking()의 함수 정의로 시작합니다. SleepTrackerViewModel 파일의 onCleared() 위에 클릭 핸들러를 배치할 수 있습니다.
fun onStartTracking() {}
  1. onStartTracking() 내에서 uiScope의 코루틴을 실행합니다. 이 결과가 있어야 계속 진행하고 UI를 업데이트할 수 있기 때문입니다.
uiScope.launch {}
  1. 코루틴 실행 내에서 현재 시간을 시작 시간으로 캡처하는 새 SleepNight를 만듭니다.
        val newNight = SleepNight()
  1. 코루틴 실행 내에서 insert()을 호출하여 newNight을 데이터베이스에 삽입합니다. 이 insert() 정지 함수를 아직 정의하지 않았기 때문에 오류가 표시됩니다. (이름이 같은 DAO 함수가 아닙니다.)
       insert(newNight)
  1. 코루틴 실행 내부에서도 tonight를 업데이트합니다.
       tonight.value = getTonightFromDatabase()
  1. onStartTracking() 아래에서 SleepNight을 인수로 사용하는 private suspend 함수로 insert()을 정의합니다.
private suspend fun insert(night: SleepNight) {}
  1. insert()의 본문에서 I/O 컨텍스트에서 코루틴을 실행하고 DAO에서 insert()을 호출하여 밤을 데이터베이스에 삽입합니다.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. fragment_sleep_tracker.xml 레이아웃 파일에서 이전에 설정한 데이터 결합의 마법을 사용하여 onStartTracking()의 클릭 핸들러를 start_button에 추가합니다. @{() -> 함수 표기법은 인수를 사용하지 않고 sleepTrackerViewModel에서 클릭 핸들러를 호출하는 람다 함수를 만듭니다.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. 앱을 빌드하고 실행합니다. Start 버튼을 탭합니다. 이 작업으로 데이터가 생성되지만 아직 아무것도 표시되지 않습니다. 다음 단계에서 이 문제를 해결합니다.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

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

3단계: 데이터 표시

SleepTrackerViewModel에서 nights 변수는 LiveData를 참조합니다. DAO의 getAllNights()LiveData를 반환하기 때문입니다.

데이터베이스의 데이터가 변경될 때마다 LiveData nights가 업데이트되어 최신 데이터를 표시하는 Room 기능입니다. LiveData를 명시적으로 설정하거나 업데이트할 필요가 없습니다. Room는 데이터베이스와 일치하도록 데이터를 업데이트합니다.

하지만 텍스트 뷰에 nights를 표시하면 객체 참조가 표시됩니다. 객체의 콘텐츠를 보려면 데이터를 형식이 지정된 문자열로 변환하세요. nights가 데이터베이스에서 새 데이터를 수신할 때마다 실행되는 Transformation 맵을 사용합니다.

  1. Util.kt 파일을 열고 formatNights() 정의와 연결된 import 문에 대한 코드의 주석 처리를 삭제합니다. Android 스튜디오에서 코드의 주석을 해제하려면 //로 표시된 코드를 모두 선택하고 Cmd+/ 또는 Control+/를 누릅니다.
  2. formatNights()는 HTML 형식의 문자열인 Spanned 유형을 반환합니다.
  3. strings.xml을 엽니다. 수면 데이터를 표시하기 위해 문자열 리소스의 형식을 지정하는 데 CDATA가 사용됩니다.
  4. SleepTrackerViewModel을 엽니다. SleepTrackerViewModel 클래스에서 uiScope 정의 아래에 nights이라는 변수를 정의합니다. 데이터베이스에서 모든 밤을 가져와 nights 변수에 할당합니다.
private val nights = database.getAllNights()
  1. nights 정의 바로 아래에 nightsnightsString로 변환하는 코드를 추가합니다. Util.kt.
    에서 formatNights() 함수를 사용합니다. Transformations 클래스에서 map() 함수에 nights를 전달합니다.
    문자열 리소스에 액세스하려면 매핑 함수를 formatNights()를 호출하는 것으로 정의하세요. nightsResources 객체를 제공합니다.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. fragment_sleep_tracker.xml 레이아웃 파일을 엽니다. 이제 TextViewandroid:text 속성에서 리소스 문자열을 nightsString 참조로 바꿀 수 있습니다.
"@{sleepTrackerViewModel.nightsString}"
  1. 코드를 다시 빌드하고 앱을 실행합니다. 이제 시작 시간이 포함된 모든 수면 데이터가 표시됩니다.
  2. 시작 버튼을 몇 번 더 탭하면 더 많은 데이터가 표시됩니다.

다음 단계에서는 Stop 버튼의 기능을 사용 설정합니다.

4단계: 중지 버튼의 클릭 핸들러 추가

이전 단계와 동일한 패턴을 사용하여 SleepTrackerViewModel.에서 중지 버튼의 클릭 핸들러를 구현합니다.

  1. ViewModelonStopTracking()를 추가합니다. uiScope에서 코루틴을 실행합니다. 종료 시간이 아직 설정되지 않은 경우 endTimeMilli를 현재 시스템 시간으로 설정하고 야간 데이터를 사용하여 update()를 호출합니다.

    Kotlin에서 return@label 문법은 여러 중첩 함수 중에서 이 문이 반환되는 함수를 지정합니다.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. insert()를 구현하는 데 사용한 것과 동일한 패턴을 사용하여 update()를 구현합니다.
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. 클릭 핸들러를 UI에 연결하려면 fragment_sleep_tracker.xml 레이아웃 파일을 열고 stop_button에 클릭 핸들러를 추가합니다.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. 앱을 빌드하고 실행합니다.
  2. 시작을 탭한 다음 중지를 탭합니다. 시작 시간, 종료 시간, 값이 없는 수면의 질, 수면 시간이 표시됩니다.

5단계: Clear 버튼의 클릭 핸들러 추가

  1. 마찬가지로 onClear()clear()을 구현합니다.
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. 클릭 핸들러를 UI에 연결하려면 fragment_sleep_tracker.xml를 열고 clear_button에 클릭 핸들러를 추가합니다.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. 앱을 빌드하고 실행합니다.
  2. 지우기를 탭하여 모든 데이터를 삭제합니다. 그런 다음 시작중지를 탭하여 새 데이터를 만듭니다.

Android 스튜디오 프로젝트: TrackMySleepQualityCoroutines

  • ViewModel, ViewModelFactory, 데이터 결합을 사용하여 앱의 UI 아키텍처를 설정합니다.
  • 계속 UI를 원활하게 실행하려면 각종 데이터베이스 작업 같은 장기 실행 작업에 코루틴을 사용합니다.
  • 코루틴은 비동기이며 비차단 형태이며 suspend 함수를 사용하여 비동기 코드를 순차 코드로 만듭니다.
  • 코루틴이 suspend로 표시된 함수를 호출하면 일반 함수 호출처럼 해당 함수가 반환될 때까지 차단되는 대신 결과가 준비될 때까지 실행이 일시 중지됩니다. 그런 다음 결과와 함께 중단된 지점에서 다시 시작합니다.
  • 차단일시중지의 차이점은 스레드가 차단되면 다른 작업이 발생하지 않는다는 것입니다. 스레드가 정지되면 결과가 제공될 때까지 다른 작업이 실행됩니다.

코루틴을 실행하려면 작업, 디스패처, 범위가 필요합니다.

  • 기본적으로 작업은 취소할 수 있는 모든 것입니다. 모든 코루틴에는 작업이 있으며 작업을 사용하여 코루틴을 취소할 수 있습니다.
  • 디스패처는 다양한 스레드에서 실행되도록 코루틴을 전송합니다. Dispatcher.Main는 기본 스레드에서 작업을 실행하고 Dispartcher.IO는 차단 I/O 작업을 공유 스레드 풀로 오프로드하는 데 사용됩니다.
  • 스코프는 작업 및 디스패처를 비롯한 정보를 결합하여 코루틴이 실행되는 컨텍스트를 정의합니다. 범위는 코루틴을 추적합니다.

데이터베이스 작업을 트리거하는 클릭 핸들러를 구현하려면 다음 패턴을 따르세요.

  1. 결과가 UI에 영향을 미치므로 기본 스레드나 UI 스레드에서 실행되는 코루틴을 실행합니다.
  2. 결과를 기다리는 동안 UI 스레드가 차단되지 않도록 정지 함수를 호출하여 장기 실행 작업을 실행합니다.
  3. 장기 실행 작업은 UI와 관련이 없으므로 I/O 컨텍스트로 전환합니다. 이렇게 하면 이러한 종류의 작업을 위해 최적화되고 예약된 스레드 풀에서 작업을 실행할 수 있습니다.
  4. 그런 다음 데이터베이스 함수를 호출하여 작업을 실행합니다.

객체가 변경될 때마다 Transformations 맵을 사용하여 LiveData 객체에서 문자열을 만듭니다.

Udacity 과정:

Android 개발자 문서:

기타 문서 및 도움말:

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.

  • 필요한 경우 과제를 할당합니다.
  • 과제 제출 방법을 학생에게 알립니다.
  • 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.

이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.

다음 질문에 답하세요

질문 1

다음 중 코루틴의 장점은 무엇인가요?

  • 비차단
  • 비동기식으로 실행됩니다.
  • 기본 스레드가 아닌 다른 스레드에서 실행될 수 있습니다.
  • 항상 앱이 더 빨리 실행되도록 해 줍니다.
  • 예외를 사용할 수 있습니다.
  • 선형 코드로 작성되거나 읽을 수 있습니다.

질문 2

정지 함수란 무엇인가요?

  • suspend 키워드로 주석 처리된 일반 함수입니다.
  • 코루틴 내에서 호출할 수 있는 함수입니다.
  • 정지 함수가 실행되는 동안 호출 스레드가 정지됩니다.
  • 정지 함수는 항상 백그라운드에서 실행해야 합니다.

질문 3

스레드 차단과 스레드 정지의 차이점은 무엇인가요? 참인 것을 모두 선택하세요.

  • 실행이 차단되면 차단된 스레드에서는 다른 작업을 실행할 수 없습니다.
  • 실행이 정지된 경우 스레드는 오프로드된 작업이 완료되기를 기다리면서 다른 작업을 할 수 있습니다.
  • 스레드가 아무것도 하지 않으면서 기다리지 않을 수 있으므로 정지하는 것이 더 효율적입니다.
  • 차단되든 정지되든 실행은 코루틴 결과를 계속 대기하다가 진행합니다.

다음 강의 시작: 6.3 LiveData를 사용하여 버튼 상태 제어

이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.