이 Codelab에서는 Android 앱에서 Kotlin 코루틴을 사용하는 방법을 알아봅니다. 새 코루틴은 콜백의 필요성을 줄여 코드를 단순화할 수 있는 백그라운드 스레드를 관리하는 새로운 방법입니다. 코루틴은 데이터베이스 액세스나 네트워크 액세스와 같은 장기 실행 작업의 비동기 콜백을 순차적 코드로 변환하는 Kotlin 기능입니다.
실행할 작업에 관한 코드 스니펫은 아래와 같습니다.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
콜백 기반 코드는 코루틴을 사용하여 순차 코드로 변환됩니다.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
장기 실행 작업에 콜백 스타일을 사용하는 아키텍처 구성요소를 사용하여 빌드된 기존 앱으로 시작합니다.
이 Codelab을 완료하면 앱에서 코루틴을 사용하여 네트워크에서 데이터를 로드할 수 있는 충분한 경험을 얻게 되고 코루틴을 앱에 통합할 수 있게 됩니다. 또한 코루틴 권장사항과 코루틴을 사용하는 코드를 대상으로 테스트를 작성하는 방법을 알아봅니다.
기본 요건
- 아키텍처 구성요소
ViewModel
,LiveData
,Repository
,Room
에 관한 기본 지식 - 확장 함수 및 람다를 포함한 Kotlin 구문 사용 경험
- Android에서 기본 스레드, 백그라운드 스레드, 콜백을 비롯한 스레드를 사용하는 방법에 관한 기본적인 이해
실행할 작업
- 코루틴으로 작성된 코드를 호출하여 결과를 얻습니다.
- 정지 함수를 사용하여 비동기 코드를 순차 코드로 만듭니다.
launch
및runBlocking
를 사용하여 코드가 실행되는 방식을 제어합니다.suspendCoroutine
를 사용하여 기존 API를 코루틴으로 변환하는 기법을 알아보세요.- 아키텍처 구성요소와 함께 코루틴 사용
- 코루틴 테스트를 위한 권장사항을 알아봅니다.
필요한 항목
- Android 스튜디오 3.5 (Codelab은 다른 버전에서 작동할 수도 있지만 일부 내용이 누락되거나 다르게 표시될 수 있습니다.)
이 Codelab을 진행하는 동안 코드 버그, 문법 오류, 불명확한 문구 등의 문제가 발생하면 Codelab 왼쪽 하단에 있는 오류 신고 링크를 통해 신고해 주세요.
코드 다운로드
다음 링크를 클릭하여 이 Codelab의 모든 코드를 다운로드합니다.
또는 다음 명령어를 사용하여 명령줄에서 GitHub 저장소를 클론합니다.
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
자주 묻는 질문자주 묻는 질문
먼저 시작 샘플 앱이 어떤 모습인지 살펴보겠습니다. 다음 안내에 따라 Android 스튜디오에서 샘플 앱을 엽니다.
kotlin-coroutines
ZIP 파일을 다운로드한 경우 파일의 압축을 풉니다.- Android 스튜디오에서
coroutines-codelab
프로젝트를 엽니다. start
애플리케이션 모듈을 선택합니다.- Run 버튼을 클릭하고 Android Lollipop을 실행해야 하는 에뮬레이터를 선택하거나 Android 기기를 연결합니다 (최소 SDK는 21임). Kotlin 코루틴 화면이 표시됩니다.
이 시작 앱은 스레드를 사용하여 화면을 누른 후 짧은 지연 시간을 늘립니다. 또한 네트워크에서 새 제목을 가져와서 화면에 표시합니다. 지금 사용해 보면 잠시 후에 수치와 메시지가 변경됩니다. 이 Codelab에서는 이 애플리케이션을 코루틴을 사용하도록 변환합니다.
이 앱은 아키텍처 구성요소를 사용하여 MainActivity
의 UI 코드를 MainViewModel
의 애플리케이션 로직과 분리합니다. 잠시 시간을 내어 프로젝트 구조를 숙지하세요.
MainActivity
은 UI를 표시하고 클릭 리스너를 등록하며Snackbar
를 표시할 수 있습니다.MainViewModel
에 이벤트를 전달하고MainViewModel
의LiveData
에 따라 화면을 업데이트합니다.MainViewModel
가onMainViewClicked
의 이벤트를 처리하고LiveData.
를 사용하여MainActivity
와 통신합니다.Executors
는 백그라운드 스레드에서 작업을 실행할 수 있는BACKGROUND,
를 정의합니다.TitleRepository
는 네트워크에서 결과를 가져와서 데이터베이스에 저장합니다.
프로젝트에 코루틴 추가
Kotlin에서 코루틴을 사용하려면 프로젝트의 build.gradle (Module: app)
파일에 coroutines-core
라이브러리를 포함해야 합니다. Codelab 프로젝트에서는 이미 이 작업을 완료했으므로 Codelab을 완료하기 위해 이 작업을 수행할 필요는 없습니다.
Android에서의 코루틴은 핵심 라이브러리로 사용할 수 있으며 Android 관련 확장 프로그램은 다음과 같습니다.
- kotlinx-corountines-core - Kotlin에서 코루틴을 사용하는 기본 인터페이스
- kotlinx-coroutines-android - 코루틴에서 Android 기본 스레드 지원
시작 앱은 이미 build.gradle.
에 종속 항목이 포함되어 있습니다. 새 앱 프로젝트를 만들 때 build.gradle (Module: app)
를 열고 프로젝트에 코루틴 종속 항목을 추가해야 합니다.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
Android에서는 기본 스레드를 차단하지 않는 것이 중요합니다. 기본 스레드는 UI에 관한 모든 업데이트를 처리하는 단일 스레드입니다. 또한 모든 클릭 핸들러와 기타 UI 콜백을 호출하는 스레드도 있습니다. 따라서 최적의 사용자 경험을 보장하기 위해 원활하게 실행되어야 합니다.
앱이 눈에 띄는 일시중지 없이 사용자에게 표시되도록 하려면 기본 스레드가 16ms 이상(약 60fps) 화면을 업데이트해야 합니다. 대용량 JSON 데이터 세트 파싱, 데이터베이스에 데이터 쓰기, 네트워크에서 데이터 가져오기 등 많은 일반적인 작업이 이보다 오래 걸립니다. 따라서 기본 스레드에서 이와 같은 코드를 호출하면 앱이 일시중지되거나 끊기거나 멈출 수 있습니다. 그리고 기본 스레드를 너무 오랫동안 차단하면 앱이 비정상 종료될 수 있으며 Application Not Responding 대화상자가 표시될 수도 있습니다.
아래 동영상을 통해 기본 안전성을 도입하여 코루틴이 Android에서 이 문제를 해결하는 방법을 알아보세요.
콜백 패턴
기본 스레드를 차단하지 않고 장기 실행 작업을 이행하는 한 가지 패턴은 콜백입니다. 콜백을 사용함으로써 백그라운드 스레드에서 장기 실행 작업을 시작할 수 있습니다. 작업이 완료되면 콜백이 호출되어 기본 스레드의 결과를 알립니다.
콜백 패턴의 예를 살펴보겠습니다.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
이 코드는 @UiThread
로 주석 처리되므로 기본 스레드에서 실행하기에 충분한 속도로 실행되어야 합니다. 즉, 다음 화면 업데이트가 지연되지 않도록 매우 빠르게 반환해야 합니다. 하지만 slowFetch
이 완료되는 데는 몇 초 또는 몇 분이 걸리므로 기본 스레드는 결과를 기다릴 수 없습니다. show(result)
콜백을 사용하면 slowFetch
가 백그라운드 스레드에서 실행되고 준비가 되면 결과를 반환할 수 있습니다.
코루틴을 사용하여 콜백 삭제
콜백은 훌륭한 패턴이지만 몇 가지 단점이 있습니다. 콜백을 매우 많이 사용하는 코드는 읽기 어렵고 추론하기가 더 어려워질 수 있습니다. 또한 콜백은 예외와 같은 일부 언어 기능의 사용을 허용하지 않습니다.
Kotlin 코루틴을 사용하면 콜백 기반 코드를 순차 코드로 변환할 수 있습니다. 순차적으로 작성된 코드는 일반적으로 읽기가 더 쉬우며 예외와 같은 언어 기능을 사용할 수도 있습니다.
결국 정확히 동일한 작업을 합니다. 즉, 장기 실행 작업에서 결과를 사용할 수 있을 때까지 기다렸다가 실행을 계속합니다. 하지만 코드에서는 완전히 다르게 보입니다.
키워드 suspend
은 코루틴에서 사용할 수 있는 함수 또는 함수 유형을 표시하는 방법입니다. 코루틴이 suspend
로 표시된 함수를 호출하면 함수는 일반 함수 호출처럼 차단될 때까지 차단되는 대신 결과가 준비될 때까지 실행을 정지하고, 결과와 함께 중단한 위치에서 다시 시작합니다. 결과를 기다리는 동안 정지된 동안에는 다른 함수 또는 코루틴을 실행할 수 있도록 실행 중인 스레드를 차단 해제합니다.
예를 들어 아래 코드에서 makeNetworkRequest()
와 slowFetch()
은 모두 suspend
함수입니다.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
콜백 버전과 마찬가지로 makeNetworkRequest
는 @UiThread
로 표시되므로 기본 스레드에서 즉시 반환해야 합니다. 즉, 일반적으로 slowFetch
와 같은 차단 메서드를 호출할 수 없습니다. 여기서 suspend
키워드는 놀라운 역할을 합니다.
콜백 기반 코드에 비해 코루틴 코드는 코드가 더 적은 현재 스레드를 차단 해제한 것과 동일한 결과를 얻습니다. 순차 스타일로 인해 여러 콜백을 만들지 않고도 장기 실행 작업을 쉽게 연결할 수 있습니다. 예를 들어 두 네트워크 엔드포인트에서 결과를 가져와서 데이터베이스에 저장하는 코드는 콜백이 없는 코루틴에서 함수로 작성할 수 있습니다. 방법은 다음과 같습니다.
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
다음 섹션에서 코루틴을 샘플 앱에 소개합니다.
이 연습에서는 지연 후 메시지를 표시하는 코루틴을 작성합니다. 시작하려면 Android 스튜디오에서 모듈 start
가 열려 있는지 확인하세요.
CoroutineScope 이해
Kotlin에서 모든 코루틴은 CoroutineScope
내에서 실행됩니다. 범위는 전체 작업에 걸쳐 코루틴의 전체 기간을 제어합니다. 범위의 작업을 취소하면 그 범위에서 시작된 코루틴이 모두 취소됩니다. Android에서는 예를 들어 사용자가 Activity
또는 Fragment
에서 벗어날 때 범위를 사용하여 실행 중인 모든 코루틴을 취소할 수 있습니다. 범위를 사용하면 기본 디스패처를 지정할 수도 있습니다. 디스패처는 코루틴을 실행하는 스레드를 제어합니다.
UI에서 시작된 코루틴의 경우 일반적으로 Android의 기본 스레드인 Dispatchers.Main
에서 시작하는 것이 좋습니다. Dispatchers.Main
에 시작된 코루틴이 정지된 동안에는 기본 스레드를 차단하지 않습니다. ViewModel
코루틴은 거의 항상 기본 스레드의 UI를 업데이트하므로 기본 스레드에서 코루틴을 시작하면 스레드 스레드가 추가로 절약됩니다. 기본 스레드에서 시작된 코루틴은 코루틴이 시작된 후 언제든지 디스패처를 전환할 수 있습니다. 예를 들어 다른 디스패처를 사용하여 기본 스레드에서 큰 JSON 결과를 파싱할 수 있습니다.
viewModelScope 사용
AndroidX lifecycle-viewmodel-ktx
라이브러리는 UI 관련 코루틴을 시작하도록 구성된 ViewModel에 CoroutineScope를 추가합니다. 이 라이브러리를 사용하려면 프로젝트의 build.gradle (Module: start)
파일에 라이브러리를 포함해야 합니다. 이 단계는 Codelab 프로젝트에서 이미 완료했습니다.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
라이브러리는 viewModelScope
를 ViewModel
클래스의 확장 함수로 추가합니다. 이 범위는 Dispatchers.Main
에 바인딩되며 ViewModel
이 삭제되면 자동으로 취소됩니다.
스레드에서 코루틴으로 전환
MainViewModel.kt
에서 다음 코드와 함께 다음 TODO를 찾습니다.
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
이 코드는 util/Executor.kt
에 정의된 BACKGROUND ExecutorService
를 사용하여 백그라운드 스레드에서 실행됩니다. sleep
가 현재 스레드에서 차단되므로 기본 스레드에서 호출되면 UI가 정지됩니다. 사용자가 기본 뷰를 클릭한 후 1초 후에 스낵바를 요청합니다.
코드에서 BACKGROUND를 삭제하고 다시 실행하면 이를 확인할 수 있습니다. 로드 중 스피너가 표시되지 않고 1초 후 모든 상태가 최종 상태로 돌아갑니다.
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
updateTaps
를 같은 작업을 하는 코루틴 기반 코드로 바꿉니다. launch
및 delay
을 가져와야 합니다.
MainViewModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
이 코드는 같은 작업을 하며 1초 후에 스낵바를 표시합니다. 하지만 몇 가지 중요한 차이점이 있습니다.
viewModelScope.
launch
이(가)viewModelScope
에서 코루틴을 시작합니다. 즉,viewModelScope
에 전달한 작업이 취소되면 이 작업/범위의 모든 코루틴이 취소됩니다.delay
가 반환되기 전에 사용자가 활동을 종료한 경우 ViewModel을 제거할 때onCleared
가 호출되면 이 코루틴은 자동으로 취소됩니다.viewModelScope
의 기본 디스패처가Dispatchers.Main
이므로 이 코루틴은 기본 스레드에서 실행됩니다. 다양한 스레드 사용 방법을 나중에 살펴보겠습니다.delay
함수는suspend
함수입니다. Android 스튜디오에서는 왼쪽 여백의 아이콘으로 표시됩니다. 이 코루틴은 기본 스레드에서 실행되지만,delay
는 1초 동안 스레드를 차단하지 않습니다. 대신 디스패처는 다음 문에서 코루틴이 1초 후에 재개되도록 예약합니다.
실행해보세요. 기본 뷰를 클릭하면 1초 후 스낵바가 표시됩니다.
다음 섹션에서는 이 함수를 테스트하는 방법을 살펴보겠습니다.
이 연습에서는 방금 작성한 코드를 위한 테스트를 작성합니다. 이 연습에서는 kotlinx-coroutines-test 라이브러리를 사용하여 Dispatchers.Main
에서 실행되는 코루틴을 테스트하는 방법을 보여줍니다. 이 Codelab의 뒷부분에서는 코루틴과 직접 상호작용하는 테스트를 구현합니다.
기존 코드 검토
androidTest
폴더에서 MainViewModelTest.kt
을 엽니다.
MainViewModelTest.kt를 참조하세요.
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
규칙은 JUnit에서 테스트를 실행하기 전과 후에 코드를 실행하는 방법입니다. 다음 두 가지 규칙을 사용하여 오프기기 테스트에서 MainViewModel을 테스트할 수 있습니다.
InstantTaskExecutorRule
는 각 작업을 동기식으로 실행하도록LiveData
를 구성하는 JUnit 규칙입니다.MainCoroutineScopeRule
는 이 코드베이스의 맞춤 규칙으로,kotlinx-coroutines-test
의TestCoroutineDispatcher
를 사용하도록Dispatchers.Main
를 구성합니다. 이렇게 하면 테스트용 가상 클록을 발전시키고 코드가 단위 테스트에서Dispatchers.Main
를 사용할 수 있습니다.
setup
메서드에서 테스트 가짜를 사용하여 MainViewModel
의 새 인스턴스가 생성됩니다. 이 인스턴스는 실제 네트워크나 데이터베이스를 사용하지 않고 테스트를 작성하는 데 도움이 되도록 시작 코드에 제공된 네트워크 및 데이터베이스의 가짜 구현입니다.
이 테스트에서 가짜는 MainViewModel
의 종속 항목을 충족하는 경우에만 필요합니다. 이 Codelab의 뒷부분에서는 코루틴을 지원하도록 가짜를 업데이트합니다.
코루틴을 제어하는 테스트 작성
기본 뷰를 클릭한 후 1초 후에 탭이 업데이트되도록 하는 새 테스트를 추가합니다.
MainViewModelTest.kt를 참조하세요.
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
onMainViewClicked
를 호출하면 방금 만든 코루틴이 실행됩니다. 이 테스트는 onMainViewClicked
가 호출된 직후 탭 텍스트를 \tapt;0 taps" 상태로 유지하며 1초 후에 "1 taps"로 업데이트되는지 확인합니다.
이 테스트는 가상 시간을 사용하여 onMainViewClicked
에서 실행된 코루틴 실행을 제어합니다. MainCoroutineScopeRule
를 사용하면 Dispatchers.Main
에서 실행되는 코루틴 실행을 일시중지, 다시 시작, 제어할 수 있습니다. 여기서 advanceTimeBy(1_000)
를 호출하면 기본 디스패처가 1초 후에 재개되도록 예약된 코루틴을 즉시 실행합니다.
이 테스트는 완전히 확정적이므로 항상 동일한 방식으로 실행됩니다. 또한 Dispatchers.Main
에서 실행된 코루틴 실행을 완전히 제어할 수 있으므로 값이 설정될 때까지 1초를 기다리지 않아도 됩니다.
기존 테스트 실행
- 편집기에서 클래스 이름
MainViewModelTest
를 마우스 오른쪽 버튼으로 클릭하여 컨텍스트 메뉴를 엽니다. - 컨텍스트 메뉴에서 Run 'MainViewModelTest'를 선택합니다.
- 향후 실행에서는 툴바의 버튼 옆에 있는 구성에서 이 테스트 구성을 선택할 수 있습니다. 기본적으로 구성은 MainViewModelTest로 호출됩니다.
테스트 통과가 표시됩니다. 실행하는 데 1초도 걸리지 않습니다.
다음 연습에서는 기존 콜백을 사용하여 코루틴을 사용하는 방법을 알아봅니다.
이 단계에서는 코루틴을 사용하도록 저장소 변환을 시작합니다. 이를 위해 ViewModel
, Repository
, Room
및 Retrofit
에 코루틴을 추가합니다.
코루틴을 사용하도록 전환하기 전에 아키텍처의 각 부분이 어떤 역할을 하는지 이해하는 것이 좋습니다.
MainDatabase
는Title
를 저장하고 로드하는 Room을 사용하여 데이터베이스를 구현합니다.MainNetwork
는 새 제목을 가져오는 네트워크 API를 구현합니다. 이 어댑터는 Retrofit을 사용하여 제목을 가져옵니다.Retrofit
는 오류 또는 모의 데이터를 무작위로 반환하도록 구성되지만, 실제 네트워크 요청을 하는 것처럼 동작합니다.TitleRepository
은 네트워크 및 데이터베이스의 데이터를 결합하여 제목을 가져오거나 새로고침하는 단일 API를 구현합니다.MainViewModel
는 화면 상태를 나타내고 이벤트를 처리합니다. 사용자가 화면을 탭할 때 제목을 새로고침하도록 저장소에 지시합니다.
네트워크 요청은 UI 이벤트에 의해 구동되며 이를 기반으로 코루틴을 시작하려고 하므로 코루틴 사용을 시작하는 자연스러운 위치는 ViewModel
입니다.
콜백 버전
MainViewModel.kt
를 열어 refreshTitle
선언을 확인합니다.
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
이 함수는 사용자가 화면을 클릭할 때마다 호출되며, 저장소가 제목을 새로고침하고 데이터베이스에 새 제목을 작성하도록 합니다.
이 구현은 콜백을 사용하여 몇 가지 작업을 실행합니다.
- 쿼리를 시작하기 전에
_spinner.value = true
와 함께 로드 스피너를 표시합니다. - 결과가 나오면
_spinner.value = false
를 사용하여 로드 스피너를 삭제합니다. - 오류가 발생하면 스낵바에 지시를 표시하고 스피너를 지웁니다.
onCompleted
콜백은 title
를 전달하지 않습니다. 모든 제목을 Room
데이터베이스에 쓰기 때문에 UI가 Room
가 업데이트된 LiveData
'를 관찰하여 현재 제목으로 업데이트됩니다.
코루틴 업데이트에서는 똑같은 동작을 유지합니다. Room
데이터베이스와 같이 식별 가능한 데이터 소스를 사용하여 UI를 자동으로 최신 상태로 유지하는 좋은 패턴입니다.
코루틴 버전
코루틴으로 refreshTitle
을 다시 작성해 보겠습니다.
바로 필요하기 때문에 저장소(TitleRespository.kt
)에 빈 정지 함수를 만듭니다. suspend
연산자를 사용하여 코루틴과 호환된다고 Kotlin에 알리는 새 함수를 정의합니다.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
이 Codelab을 완료하면 Retrofit과 Room을 사용하여 새 제목을 가져오고 코루틴을 사용하여 데이터베이스에 쓰도록 업데이트합니다. 지금은 이를 위해 500밀리초의 시간을 소비한 다음 작업을 계속합니다.
MainViewModel
에서 refreshTitle
의 콜백 버전을 새 코루틴을 시작하는 버전으로 바꿉니다.
MainViewModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
이 함수를 단계별로 살펴보겠습니다.
viewModelScope.launch {
코루틴이 탭 수를 업데이트하는 것과 마찬가지로 먼저 viewModelScope
에서 새 코루틴을 실행합니다. 이렇게 하면 OK인 Dispatchers.Main
을 사용합니다. refreshTitle
이 네트워크 요청과 데이터베이스 쿼리를 실행하더라도 코루틴을 사용하여 기본 안전 인터페이스를 노출할 수 있습니다. 즉, 기본 스레드에서 안전하게 호출하세요.
viewModelScope
를 사용하고 있으므로 사용자가 이 화면에서 벗어나면 이 코루틴에서 시작된 작업이 자동으로 취소됩니다. 즉, 추가 네트워크 요청을 하거나 데이터베이스를 쿼리하지 않습니다.
다음 몇 줄의 코드는 실제로 repository
에서 refreshTitle
를 호출합니다.
try {
_spinner.value = true
repository.refreshTitle()
}
이 코루틴은 어떤 작업도 로드하기 전에 로드 스피너를 시작합니다. 그런 다음 일반 함수처럼 refreshTitle
를 호출합니다. 그러나 refreshTitle
는 정지 함수이므로 일반 함수와 다르게 실행됩니다.
콜백을 전달할 필요가 없습니다. 코루틴은 refreshTitle
에서 재개될 때까지 정지됩니다. 일반 차단 함수 호출과 유사하게 보이지만 네트워크 및 데이터베이스 쿼리가 완료될 때까지 자동으로 대기하고 기본 스레드를 차단하지 않습니다.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
정지 함수의 예외는 일반 함수의 오류와 마찬가지로 작동합니다. 정지 함수에서 오류가 발생하면 호출자에 발생합니다. 따라서 완전히 실행되지 않더라도 일반 try/catch 블록을 사용하여 처리할 수 있습니다. 이 기능은 모든 콜백에 대해 커스텀 오류 처리를 빌드하는 대신 오류 처리를 위해 내장된 언어 지원을 사용할 수 있으므로 유용합니다.
그리고 코루틴에서 예외가 발생하면 코루틴은 기본적으로 상위 요소를 취소합니다. 즉, 여러 관련 작업을 함께 쉽게 취소할 수 있습니다.
그런 다음 마지막으로 블록에서 쿼리가 실행된 후에 스피너가 항상 꺼지도록 할 수 있습니다.
start 구성을 선택한 다음 를 눌러 애플리케이션을 다시 실행합니다. 아무 곳이나 탭하면 로드 스피너가 표시됩니다. Google에서 아직 네트워크 또는 데이터베이스를 연결하지 않았으므로 제목은 그대로 유지됩니다.
다음 연습에서는 실제로 작동하도록 저장소를 업데이트합니다.
이 연습에서는 TitleRepository
의 작업 버전을 구현하기 위해 코루틴이 실행되는 스레드를 전환하는 방법을 알아봅니다.
refreshTitle에서 기존 콜백 코드 검토
TitleRepository.kt
를 열고 기존 콜백 기반 구현을 검토합니다.
TitleRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
TitleRepository.kt
에서 refreshTitleWithCallbacks
메서드는 로드와 오류 상태를 호출자에 전달하는 콜백으로 구현됩니다.
이 함수는 새로고침을 구현하기 위해 몇 가지 작업을 합니다.
BACKGROUND
ExecutorService
대화목록을 다른 대화목록으로 전환- 차단
execute()
메서드를 사용하여fetchNextTitle
네트워크 요청을 실행합니다. 그러면 현재 스레드에서 네트워크 요청이 실행됩니다. 이 경우에는BACKGROUND
의 스레드 중 하나입니다. - 결과가 성공하면
insertTitle
를 사용하여 데이터베이스에 저장하고onCompleted()
메서드를 호출합니다. - 결과가 성공하지 못했거나 예외가 있는 경우 onError 메서드를 호출하여 호출 실패에 관해 발신자에게 알립니다.
이 콜백 기반 구현은 기본 스레드를 차단하지 않으므로 기본 안전성이 보장됩니다. 그러나 작업이 완료되면 콜백을 사용하여 발신자에게 알려야 합니다. 또한 전환한 BACKGROUND
스레드에서도 콜백을 호출합니다.
코루틴에서 차단 호출 호출
네트워크 또는 데이터베이스에 코루틴을 도입하지 않고 코루틴을 사용하여 이 코드를 기본 안전성으로 설정할 수 있습니다. 이렇게 하면 콜백을 제거하고 결과를 호출한 스레드로 다시 전달할 수 있습니다.
코루틴 내에서 대량 목록 정렬 및 필터링, 디스크에서 읽기와 같은 차단 또는 CPU 집약적인 작업을 실행해야 할 때 언제든지 이 패턴을 사용할 수 있습니다.
디스패처 간에 전환하기 위해 코루틴은 withContext
를 사용합니다. withContext
를 호출하면 람다 전용의 다른 디스패처로 전환되었다가 이 람다 결과와 함께 호출했던 디스패처로 돌아옵니다.
기본적으로 Kotlin 코루틴은 Main
, IO
, Default
의 세 가지 디스패처를 제공합니다. IO 디스패처는 네트워크나 디스크에서 읽기와 같은 IO 작업에 최적화되어 있고, 기본 디스패처는 CPU 집약적인 작업에 최적화되어 있습니다.
TitleRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
이 구현은 네트워크 및 데이터베이스에 대한 차단 호출을 사용하지만 여전히 콜백 버전보다 조금 더 간단합니다.
이 코드는 계속 차단 호출을 사용합니다. execute()
와 insertTitle(...)
을 호출하면 모두 이 코루틴이 실행 중인 스레드가 차단됩니다. 그러나 withContext
를 사용하여 Dispatchers.IO
로 전환하면 IO 디스패처의 스레드 중 하나가 차단됩니다. Dispatchers.Main
에서 실행되었을 수 있는 이 메서드를 호출한 코루틴은 withContext
람다가 완료될 때까지 정지됩니다.
콜백 버전과 비교할 때 다음과 같은 두 가지 중요한 차이점이 있습니다.
withContext
는 호출한 Dispatcher(이 경우에는Dispatchers.Main
)에 다시 결과를 반환합니다.BACKGROUND
실행기 서비스의 스레드에서 콜백을 호출한 콜백 버전입니다.- 호출자는 이 함수에 콜백을 전달할 필요가 없습니다. 사용자는 정지에 의존하여 결과나 오류를 가져올 수 있습니다.
앱 다시 실행
앱을 다시 실행하면 새로운 코루틴 기반 구현이 네트워크에서 결과를 로드하는 것을 확인할 수 있습니다.
다음 단계에서는 코루틴을 Room과 Retrofit에 통합합니다.
코루틴 통합을 계속 진행하기 위해 Room의 안정적인 버전과 Retrofit에서 정지 함수 지원을 사용한 다음 정지 함수를 사용하여 방금 작성한 코드를 간소화합니다.
Room의 코루틴
먼저 MainDatabase.kt
를 열고 insertTitle
를 정지 함수로 만듭니다.
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
이렇게 하면 Room에서 쿼리를 기본 안전성으로 설정하고 백그라운드 스레드에서 자동으로 실행합니다. 그러나 코루틴 내에서만 이 쿼리를 호출할 수 있습니다.
Room에서 코루틴을 사용하기만 하면 됩니다. 정말 멋집니다.
Retrofit의 코루틴
다음으로 Retrofit과 코루틴을 통합하는 방법을 알아보겠습니다. MainNetwork.kt
를 열고 fetchNextTitle
를 정지 함수로 변경합니다.
기본 네트워크.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Retrofit과 함께 정지 함수를 사용하려면 다음 두 가지 작업을 해야 합니다.
- 함수에 정지 수정자 추가
- 반환 유형에서
Call
래퍼를 삭제합니다. 여기서는String
를 반환하지만 복잡한 json 지원 유형도 반환할 수 있습니다. Retrofit의 전체Result
에 계속 액세스 권한을 제공하려면 정지 함수에서String
대신Result<String>
를 반환하면 됩니다.
Retrofit은 정지 함수를 기본 안전성으로 자동 설정해 Dispatchers.Main
에서 직접 호출할 수 있습니다.
Room 및 Retrofit 사용
Room과 Retrofit이 정지 함수를 지원하므로 이제 저장소에서 사용할 수 있습니다. TitleRepository.kt
를 열고 차단 함수와 비교하여 정지 함수를 사용하면 로직이 어떻게 크게 간소화되는지 알아보세요.
제목Repository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
와, 많은 짧아요. 어떻게 된 것일까요? 정지 및 재개에 의존하면 코드가 훨씬 더 짧아집니다. Retrofit을 사용하면 여기에서 Call
대신 String
또는 User
객체와 같은 반환 유형을 사용할 수 있습니다. 정지 함수 내에서 Retrofit
가 백그라운드 스레드에서 네트워크 요청을 실행하고 호출이 완료되면 코루틴을 재개할 수 있으므로 안전합니다.
또한 withContext
을 삭제했습니다. Room과 Retrofit은 모두 기본 안전성을 갖춘 정지 함수를 제공하므로 Dispatchers.Main
에서 이 비동기 작업을 조정할 수 있습니다.
컴파일러 오류 수정
코루틴으로 이동하려면 일반 함수에서 정지 함수를 호출할 수 없으므로 함수의 서명을 변경해야 합니다. 이 단계에서 suspend
수정자를 추가하면 실제 프로젝트에서 정지하도록 함수를 변경하면 어떤 일이 발생하는지 보여주는 몇 가지 컴파일러 오류가 발생했습니다.
프로젝트를 살펴보고 생성된 정지로 함수를 변경하여 컴파일러 오류를 수정합니다. 다음은 각 항목의 간단한 해결 방법입니다.
TestingFakes.kt
새로운 정지 수정자를 지원하도록 테스트 모조를 업데이트합니다.
TitleDaoFake
- Alt + Enter를 눌러 헤이라시의 모든 함수에 정지 수정자를 추가합니다.
MainNetworkFake
- Alt + Enter를 눌러 헤이라시의 모든 함수에 정지 수정자를 추가합니다.
fetchNextTitle
를 이 함수로 대체
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Alt + Enter를 눌러 헤이라시의 모든 함수에 정지 수정자를 추가합니다.
fetchNextTitle
를 이 함수로 대체
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
refreshTitleWithCallbacks
함수는 더 이상 사용되지 않으므로 삭제합니다.
앱 실행
앱이 다시 실행되면 컴파일되면 ViewModel에서 Room 및 Retrofit에 코루틴을 사용하여 데이터를 로드하는 것을 확인할 수 있습니다.
축하합니다. 이 앱을 코루틴으로 완전히 전환했습니다. 마지막으로 오늘 배운 내용을 테스트하는 방법에 대해 알아보겠습니다.
이 연습에서는 suspend
함수를 직접 호출하는 테스트를 작성합니다.
refreshTitle
는 공개 API로 노출되므로 직접 테스트되므로 테스트에서 코루틴 함수를 호출하는 방법을 보여줍니다.
다음은 이전 실습에서 구현한 refreshTitle
함수입니다.
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
정지 함수를 호출하는 테스트 작성
TODOS가 두 개 있는 test
폴더에서 TitleRepositoryTest.kt
을 엽니다.
첫 번째 테스트 whenRefreshTitleSuccess_insertsRows
에서 refreshTitle
를 호출해 봅니다.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
refreshTitle
은 suspend
함수이므로 Kotlin은 코루틴이나 다른 정지 함수를 제외한 다른 함수를 호출하는 방법을 알 수 없으며, "Suspend 함수 refreshTitle은 코루틴이나 다른 정지 함수에서만 호출해야 합니다.
테스트 실행기는 코루틴에 관해 알지 못하므로 이 테스트를 정지 함수로 만들 수 없습니다. ViewModel
에서와 같이 CoroutineScope
를 사용하여 코루틴을 launch
할 수 있지만, 테스트는 반환하기 전에 코루틴을 완료해야 합니다. 테스트 함수가 반환되면 테스트가 종료됩니다. launch
로 시작하는 코루틴은 비동기 코드로, 나중에 특정 시점에 완료될 수 있습니다. 따라서 비동기 코드를 테스트하려면 코루틴이 완료될 때까지 테스트에 대기하도록 테스트에 알리는 방법이 필요합니다. launch
는 비차단 호출이므로 즉시 반환되며 함수가 반환된 후에도 코루틴을 계속 실행할 수 있습니다. 이는 테스트에서 사용할 수 없습니다. 예를 들면 다음과 같습니다.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
이 테스트는 때때로 실패합니다. launch
호출은 즉시 반환되고 테스트 사례의 나머지 부분과 동시에 실행됩니다. 테스트에서는 refreshTitle
가 아직 실행되었는지 알 수 없으며, 데이터베이스가 업데이트되었는지 확인하는 등의 어설션이 취약합니다. 또한 refreshTitle
에서 예외가 발생해도 테스트 호출 스택에서 발생하지 않습니다. 대신 GlobalScope
의 포착되지 않은 예외 핸들러에 발생합니다.
kotlinx-coroutines-test
라이브러리에는 정지 함수를 호출하는 동안 차단하는 runBlockingTest
함수가 있습니다. runBlockingTest
가 정지 함수를 호출하거나 새 코루틴을 launches
하는 경우 기본적으로 즉시 실행됩니다. 이를 정지 함수 및 코루틴을 일반 함수 호출로 변환하는 방법으로 생각할 수 있습니다.
또한 runBlockingTest
는 포착되지 않은 예외를 다시 발생시킵니다. 이를 통해 코루틴이 예외를 발생시키는 경우 더 쉽게 테스트할 수 있습니다.
하나의 코루틴으로 테스트 구현
runBlockingTest
로 refreshTitle
호출을 래핑하고 subject.refreshTitle()에서 GlobalScope.launch
래퍼를 삭제합니다.
TitleRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
이 테스트에서는 제공된 모조를 사용하여 refreshTitle
에서 데이터베이스에 'OK'가 삽입되었는지 확인합니다.
테스트가 runBlockingTest
를 호출하면 runBlockingTest
에 의해 시작된 코루틴이 완료될 때까지 차단됩니다. 그런 다음 내부에서 refreshTitle
를 호출할 때 일반 정지 및 재개 메커니즘을 사용하여 데이터베이스 행이 모조에 추가될 때까지 기다립니다.
테스트 코루틴이 완료되면 runBlockingTest
가 반환됩니다.
시간 제한 테스트 작성
네트워크 요청에 짧은 제한 시간을 추가하려고 합니다. 테스트를 먼저 작성한 후 제한 시간을 구현해 보겠습니다. 새 테스트를 만듭니다.
TitleRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
이 테스트에서는 제공된 가짜 MainNetworkCompletableFake
를 사용합니다. 가짜는 테스트가 계속될 때까지 호출자를 정지하도록 설계된 네트워크 가짜입니다. refreshTitle
에서 네트워크 요청을 시도하면 시간 초과를 테스트하려고 하므로 영구적으로 중단됩니다.
그런 다음 별도의 코루틴을 실행하여 refreshTitle
을 호출합니다. 이는 제한 시간 테스트의 핵심 부분이며, runBlockingTest
에서 만든 것과 다른 코루틴에서 시간 제한이 발생해야 합니다. 그렇게 하면 다음 줄인 advanceTimeBy(5_000)
를 호출하여 시간을 5초 앞당기고 다른 코루틴이 시간 초과되도록 할 수 있습니다.
이는 완전한 시간 제한 테스트이며 시간 제한을 구현하면 통과됩니다.
지금 실행하면 다음과 같이 됩니다.
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
runBlockingTest
의 기능 중 하나는 테스트가 완료된 후에 코루틴을 유출할 수 없다는 점입니다. 출시 코루틴과 같이 완료되지 않은 코루틴이 있으면 테스트가 끝날 때 테스트가 실패합니다.
시간 제한 추가하기
TitleRepository
를 열고 네트워크 가져오기에 5초 시간 제한을 추가합니다. 다음과 같이 withTimeout
함수를 사용하면 됩니다.
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
테스트를 실행합니다. 테스트를 실행하면 모든 테스트를 통과한 것을 확인할 수 있습니다.
다음 연습에서는 코루틴을 사용하여 고차 함수를 작성하는 방법을 알아봅니다.
이 연습에서는 일반 데이터 로드 함수를 사용하도록 MainViewModel
에서 refreshTitle
를 리팩터링합니다. 코루틴을 사용하는 고차 함수를 빌드하는 방법을 알아봅니다.
refreshTitle
의 현재 구현은 작동하지만 항상 스피너를 표시하는 일반 데이터 로드 코루틴을 만들 수 있습니다. 이는 여러 이벤트에 대한 응답으로 데이터를 로드하는 코드베이스에서 유용할 수 있으며 로드 스피너가 일관되게 표시되도록 하려고 합니다.
repository.refreshTitle()
를 제외한 모든 행에서 현재 구현을 검토하는 것은 스피너를 표시하고 오류를 표시하기 위해 상용구입니다.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
고차 함수에서 코루틴 사용
MainViewModel.kt에 다음 코드를 추가합니다.
MainViewModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
이제 이 고차 함수를 사용하도록 refreshTitle()
를 리팩터링합니다.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
로드 스피너의 표시와 오류 표시에 관한 로직을 추상화하여 데이터를 로드하는 데 필요한 실제 코드를 단순화했습니다. 스피너를 표시하거나 오류를 표시하는 것은 모든 데이터 로드에 쉽게 일반화되는 반면, 실제 데이터 소스와 대상은 매번 지정해야 합니다.
이 추상화를 빌드하기 위해 launchDataLoad
는 정지 람다인 block
인수를 사용합니다. 정지 람다를 사용하여 정지 함수를 호출할 수 있습니다. Kotlin이 이 Codelab에서 사용한 코루틴 빌더 launch
및 runBlocking
를 구현하는 방법입니다.
// suspend lambda
block: suspend () -> Unit
정지 람다를 만들려면 suspend
키워드로 시작합니다. 함수 화살표와 반환 유형 Unit
가 선언을 완료합니다.
자체적으로 정지 람다를 선언할 필요가 없지만 이렇게 반복 로직을 캡슐화하는 추상화를 만들면 유용할 수 있습니다.
이 연습에서는 WorkManager의 코루틴 기반 코드를 사용하는 방법을 알아봅니다.
WorkManager란?
Android에는 지연 가능한 백그라운드 작업을 위한 다양한 옵션이 있습니다. 이 연습에서는 WorkManager를 코루틴과 통합하는 방법을 설명합니다. WorkManager는 지연 가능한 백그라운드 작업을 지원하는 호환 가능하고 유연하며 간단한 라이브러리입니다. WorkManager는 Android의 이러한 사용 사례에 권장되는 솔루션입니다.
WorkManager는 상황별 실행과 보장된 실행을 조합하여 적용해야 하는 백그라운드 작업을 위한 아키텍처 구성요소로서 Android Jetpack의 일부입니다. 상황별 실행을 적용하면 WorkManager가 최대한 빨리 백그라운드 작업을 실행합니다. 보장된 실행을 적용하면 WorkManager가 사용자가 앱을 벗어난 경우를 비롯한 다양한 상황에서 로직을 처리하여 작업을 시작합니다.
따라서 WorkManager는 최종적으로 완료해야 하는 작업에 적합합니다.
WorkManager는 아래와 같은 작업에 사용하는 것이 적합합니다.
- 로그 업로드
- 이미지에 필터 적용 및 이미지 저장
- 주기적으로 로컬 데이터를 네트워크와 동기화
WorkManager와 함께 코루틴 사용
WorkManager는 다양한 사용 사례에 따라 기본 ListanableWorker
클래스의 다양한 구현을 제공합니다.
가장 간단한 Worker 클래스를 사용하면 WorkManager에서 동기 작업을 실행할 수 있습니다. 하지만 지금까지 코루틴을 사용하고 함수를 정지하도록 코드베이스를 변환해 왔으므로 WorkManager를 사용하는 가장 좋은 방법은 doWork()
함수를 정지 함수로 정의할 수 있는 CoroutineWorker
클래스를 사용하는 것입니다.
시작하려면 RefreshMainDataWork
앱을 엽니다. 이미 CoroutineWorker
를 확장하므로 doWork
를 구현해야 합니다.
suspend
doWork
함수 내부에서 refreshTitle()
를 호출하여 적절한 결과를 반환합니다.
TODO를 완료하면 코드는 다음과 같습니다.
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
CoroutineWorker.doWork()
는 정지 함수입니다. 더 간단한 Worker
클래스와 달리 이 코드는 WorkManager 구성에 지정된 Executor에서 실행되지 않지만 대신 coroutineContext
멤버 (기본적으로 Dispatchers.Default
)의 디스패처를 사용합니다.
CoroutineWorker 테스트
테스트 없이는 모든 코드베이스가 완성되어야 합니다.
WorkManager를 사용하면 Worker
클래스를 테스트하는 몇 가지 방법을 사용할 수 있습니다. 기존 테스트 인프라에 관해 자세히 알아보려면 문서를 참고하세요.
WorkManager v2.1에서는 ListenableWorker
클래스를 테스트하는 더 간단한 방법을 지원할 수 있는 새로운 API 세트를 도입합니다(따라서 CoroutineWorker). 코드에서는 새로운 API 중 하나인 TestListenableWorkerBuilder
를 사용합니다.
새 테스트를 추가하려면 androidTest
폴더 아래 RefreshMainDataWorkTest
파일을 업데이트합니다.
파일의 내용은 다음과 같습니다.
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
테스트에 앞서 가짜 네트워크를 삽입할 수 있도록 WorkManager
에 팩토리에 관해 알립니다.
테스트 자체는 TestListenableWorkerBuilder
를 사용하여 작업자를 만든 후 startWork()
메서드 호출을 실행할 수 있습니다.
WorkManager는 코루틴을 사용하여 API 디자인을 간소화하는 방법의 한 예에 불과합니다.
이 Codelab에서는 앱에서 코루틴을 사용하는 데 필요한 기본사항을 설명했습니다.
다루는 내용은 다음과 같습니다.
- 비동기 프로그래밍을 간소화하기 위해 UI 및 WorkManager 작업에서 Android 앱에 코루틴을 통합하는 방법
ViewModel
내의 코루틴을 사용하여 기본 스레드를 차단하지 않고 네트워크에서 데이터를 가져와 데이터베이스에 저장하는 방법ViewModel
가 완료되면 모든 코루틴을 취소하는 방법
코루틴 기반 코드의 테스트에서는 동작을 테스트하고 테스트에서 suspend
함수를 직접 호출하는 방법을 모두 다루었습니다.
자세히 알아보기
'Kotlin Flow 및 LiveData를 사용하는 고급 코루틴' Codelab에서 Android의 고급 코루틴 사용법을 알아보세요.
Kotlin 코루틴에는 이 Codelab에서 다루지 않은 많은 기능이 있습니다. Kotlin 코루틴에 관해 자세히 알아보려면 JetBrains에서 게시한 코루틴 가이드를 참고하세요. 'Kotlin 코루틴으로 앱 성능 향상'에서도 Android의 코루틴 사용 패턴을 자세히 확인할 수 있습니다.