이 Codelab에서는 Android 앱에서 Kotlin 코루틴을 사용하는 방법을 알아봅니다. 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
자주 묻는 질문(FAQ)
먼저 시작 샘플 앱이 어떤 모습인지 살펴보겠습니다. 다음 안내에 따라 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-coroutines-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마다 또는 더 자주 화면을 업데이트해야 합니다(초당 약 60프레임). 대규모 JSON 데이터 세트 파싱, 데이터베이스에 데이터 쓰기, 네트워크에서 데이터 가져오기와 같은 많은 일반적인 작업은 이보다 오래 걸립니다. 따라서 기본 스레드에서 이와 같은 코드를 호출하면 앱이 일시중지되거나 끊기거나 멈출 수 있습니다. 그리고 기본 스레드를 너무 오랫동안 차단하면 앱이 비정상 종료될 수 있으며 애플리케이션 응답 없음 대화상자가 표시될 수도 있습니다.
아래 동영상을 시청하여 코루틴이 메인 스레드 안전성을 도입하여 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
키워드는 코루틴에서 사용할 수 있는 함수 또는 함수 유형을 표시하는 Kotlin의 방법입니다. 코루틴이 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 관련 코루틴을 시작하도록 구성된 CoroutineScope를 ViewModel에 추가합니다. 이 라이브러리를 사용하려면 프로젝트의 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")
}
}
이 코드는 BACKGROUND ExecutorService
(util/Executor.kt
에 정의됨)를 사용하여 백그라운드 스레드에서 실행됩니다. 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
가 호출된 직후 탭 텍스트가 '0 taps'로 유지되고 1초 후에 '1 taps'로 업데이트되는지 확인합니다.
이 테스트에서는 가상 시간을 사용하여 onMainViewClicked
로 실행된 코루틴의 실행을 제어합니다. MainCoroutineScopeRule
를 사용하면 Dispatchers.Main
에서 실행되는 코루틴의 실행을 일시중지하거나 재개하거나 제어할 수 있습니다. 여기서는 advanceTimeBy(1_000)
를 호출하여 1초 후에 재개되도록 예약된 코루틴을 기본 디스패처가 즉시 실행하도록 합니다.
이 테스트는 완전히 결정적입니다. 즉, 항상 동일한 방식으로 실행됩니다. 또한 Dispatchers.Main
에서 실행된 코루틴을 완전히 제어할 수 있으므로 값이 설정될 때까지 1초 동안 기다릴 필요가 없습니다.
기존 테스트 실행
- 편집기에서 클래스 이름
MainViewModelTest
을 마우스 오른쪽 버튼으로 클릭하여 컨텍스트 메뉴를 엽니다. - 컨텍스트 메뉴에서
Run 'MainViewModelTest'를 선택합니다.
- 향후 실행에서는 툴바의
버튼 옆에 있는 구성에서 이 테스트 구성을 선택할 수 있습니다. 기본적으로 구성의 이름은 MainViewModelTest입니다.
테스트가 통과됩니다. 실행하는 데 1초도 걸리지 않습니다.
다음 연습에서는 기존 콜백 API에서 코루틴을 사용하도록 변환하는 방법을 알아봅니다.
이 단계에서는 코루틴을 사용하도록 저장소를 변환하기 시작합니다. 이를 위해 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
에서 새 코루틴을 실행하여 시작합니다. 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 블록을 사용하여 처리할 수 있습니다. 이렇게 하면 모든 콜백에 맞춤 오류 처리를 빌드하는 대신 오류 처리를 위해 내장된 언어 지원을 사용할 수 있으므로 유용합니다.
코루틴에서 예외를 발생시키면 해당 코루틴은 기본적으로 상위 요소를 취소합니다. 따라서 여러 관련 작업을 한 번에 쉽게 취소할 수 있습니다.
그런 다음 finally 블록에서 쿼리가 실행된 후 스피너가 항상 꺼지도록 할 수 있습니다.
start 구성을 선택한 다음 를 눌러 애플리케이션을 다시 실행하면 아무 곳이나 탭할 때 로드 스피너가 표시됩니다. 아직 네트워크나 데이터베이스를 연결하지 않았으므로 제목은 그대로 유지됩니다.
다음 실습에서는 실제로 작업을 수행하도록 저장소를 업데이트합니다.
이 연습에서는 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
스레드에서 콜백을 호출합니다.
코루틴에서 차단 호출 호출
네트워크나 데이터베이스에 코루틴을 도입하지 않고도 코루틴을 사용하여 이 코드를 main-safe로 만들 수 있습니다. 이렇게 하면 콜백을 없애고 결과를 처음 호출한 스레드에 다시 전달할 수 있습니다.
큰 목록을 정렬 및 필터링하거나 디스크에서 읽는 등 코루틴 내부에서 차단 또는 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
는 결과를 호출한 디스패처(이 경우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에서 쿼리를 main-safe로 만들고 백그라운드 스레드에서 자동으로 실행합니다. 하지만 코루틴 내에서만 이 쿼리를 호출할 수 있다는 의미이기도 합니다.
Room에서 코루틴을 사용하기 위해 해야 할 일은 이게 전부입니다. 꽤 유용하죠.
Retrofit의 코루틴
다음으로 코루틴을 Retrofit과 통합하는 방법을 알아보겠습니다. MainNetwork.kt
를 열고 fetchNextTitle
를 일시중지 함수로 변경합니다.
MainNetwork.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에서 정지 함수를 사용하려면 다음 두 가지 작업을 실행해야 합니다.
- 함수에 suspend 수정자 추가
- 반환 유형에서
Call
래퍼를 삭제합니다. 여기서는String
을 반환하지만 복잡한 json 지원 유형을 반환할 수도 있습니다. 여전히 리트로핏의 전체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
수정자를 추가하면 실제 프로젝트에서 함수를 중단하도록 변경할 경우 발생하는 상황을 보여주는 컴파일러 오류가 몇 개 생성되었습니다.
프로젝트를 살펴보고 함수를 suspend created로 변경하여 컴파일러 오류를 수정합니다. 각 문제에 대한 빠른 해결 방법은 다음과 같습니다.
TestingFakes.kt
새로운 suspend 수정자를 지원하도록 테스트 가짜 업데이트
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)
}
}
정지 함수를 호출하는 테스트 작성
할 일이 두 개 있는 test
폴더에서 TitleRepositoryTest.kt
을 엽니다.
첫 번째 테스트 whenRefreshTitleSuccess_insertsRows
에서 refreshTitle
를 호출해 보세요.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
refreshTitle
는 suspend
함수이므로 Kotlin은 코루틴이나 다른 정지 함수에서만 이를 호출하는 방법을 알고 있으며 '정지 함수 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는 지연 가능한 백그라운드 작업을 지원하는 호환 가능하고 유연하며 단순한 라이브러리입니다. Android에서는 이러한 사용 사례에 WorkManager를 사용하는 것이 좋습니다.
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
클래스와 결과적으로 CoroutineWorker를 더 간단하게 테스트할 수 있도록 지원하는 새로운 API 세트가 도입되었습니다. 코드에서는 이러한 새 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에서 게시한 코루틴 가이드를 참고하세요. Android에서 코루틴을 사용하는 방법에 관한 자세한 내용은 'Kotlin 코루틴으로 앱 성능 향상'을 참고하세요.