Kotlin 코루틴은 비동기 코드를 작성할 수 있게 하는 API를 제공합니다. Kotlin 코루틴을 사용하면 코루틴이 실행되어야 하는 시기를 관리하는 데 도움이 되는 CoroutineScope
를 정의할 수 있습니다. 각 비동기 작업은 특정 범위 내에서 실행됩니다.
수명 주기 인식 구성요소는 LiveData
와의 상호운용성 레이어와 함께 앱의 논리적 범위에 관한 코루틴을 가장 잘 지원합니다. 본 항목에서는 수명 주기 인식 구성요소와 함께 코루틴을 효과적으로 사용하는 방법을 설명합니다.
KTX 종속성 추가
본 항목에서 설명하는 기본 제공 코루틴 범위는 해당하는 각 구성요소의 KTX 확장 프로그램에 포함되어 있습니다. 이러한 범위 사용 시 적절한 종속 항목을 추가해야 합니다.
ViewModelScope
의 경우androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
이상을 사용합니다.LifecycleScope
의 경우androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
이상을 사용합니다.liveData
의 경우androidx.lifecycle:lifecycle-livedata-ktx:2.4.0
이상을 사용합니다.
수명 주기 인식 코루틴 범위
수명 주기 인식 구성요소는 앱에서 사용할 수 있는 다음 기본 제공 범위를 정의합니다.
ViewModelScope
ViewModelScope
는 앱의 각 ViewModel
을 대상으로 정의됩니다. 이 범위에서 시작된 모든 코루틴은 ViewModel
이 삭제되면 자동으로 취소됩니다. 코루틴은 ViewModel
이 활성 상태인 경우에만 실행해야 할 작업이 있을 때 유용합니다. 예를 들어 레이아웃의 일부 데이터를 계산한다면 작업의 범위를 ViewModel
로 지정하여 ViewModel
을 삭제하면 리소스를 소모하지 않도록 작업이 자동으로 취소됩니다.
다음 예와 같이 ViewModel의 viewModelScope
속성을 통해 ViewModel
의 CoroutineScope
에 액세스할 수 있습니다.
class MyViewModel: ViewModel() { init { viewModelScope.launch { // Coroutine that will be canceled when the ViewModel is cleared. } } }
LifecycleScope
LifecycleScope
는 각 Lifecycle
객체에서 정의됩니다. 이 범위에서 실행된 코루틴은 Lifecycle
이 끝날 때 제거됩니다. lifecycle.coroutineScope
또는 lifecycleOwner.lifecycleScope
속성을 통해 Lifecycle
의 CoroutineScope
에 액세스할 수 있습니다.
아래 예는 lifecycleOwner.lifecycleScope
를 사용하여 미리 계산된 텍스트를 비동기적으로 만드는 방법을 보여줍니다.
class MyFragment: Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { val params = TextViewCompat.getTextMetricsParams(textView) val precomputedText = withContext(Dispatchers.Default) { PrecomputedTextCompat.create(longTextContent, params) } TextViewCompat.setPrecomputedText(textView, precomputedText) } } }
재시작 가능한 수명 주기 인식 코루틴
Lifecycle
이 DESTROYED
일 때 lifecycleScope
가 장기 실행 작업을 자동으로 취소하는 올바른 방법을 제공하지만 Lifecycle
이 특정 상태에 있을 때 코드 블록의 실행을 시작하고 다른 상태에 있을 때 취소하려는 경우가 있을 수 있습니다. 예를 들어 Lifecycle
이 STARTED
일 때 흐름을 수집하고 STOPPED
일 때 수집을 취소하려고 할 수 있습니다. 이 방법은 UI가 화면에 표시될 때만 흐름 내보내기를 처리하여 리소스를 절약하고 앱 비정상 종료를 방지할 수 있습니다.
이러한 경우 Lifecycle
과 LifecycleOwner
는 정확히 이를 실행하는 정지 repeatOnLifecycle
API를 제공합니다. 다음 예에는 관련 Lifecycle
이 최소 STARTED
상태일 때마다 실행되고, Lifecycle
이 STOPPED
일 때 취소되는 코드 블록이 포함되어 있습니다.
class MyFragment : Fragment() { val viewModel: MyViewModel by viewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Create a new coroutine in the lifecycleScope viewLifecycleOwner.lifecycleScope.launch { // repeatOnLifecycle launches the block in a new coroutine every time the // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED. viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // Trigger the flow and start listening for values. // This happens when lifecycle is STARTED and stops // collecting when the lifecycle is STOPPED viewModel.someDataFlow.collect { // Process item } } } } }
수명 주기 인식 흐름 수집
단일 흐름에서 수명 주기 인식 수집을 진행하기만 하면 되는 경우 Flow.flowWithLifecycle()
메서드를 사용하여 코드를 단순하게 작성하면 됩니다.
viewLifecycleOwner.lifecycleScope.launch { exampleProvider.exampleFlow() .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) .collect { // Process the value. } }
하지만 여러 흐름에서 수명 주기 인식 수집을 동시에 진행해야 하는 경우 서로 다른 코루틴에서 각 흐름을 수집해야 합니다. 이 경우 repeatOnLifecycle()
을 직접 사용하는 것이 더 효율적입니다.
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // Because collect is a suspend function, if you want to // collect multiple flows in parallel, you need to do so in // different coroutines. launch { flow1.collect { /* Process the value. */ } } launch { flow2.collect { /* Process the value. */ } } } }
수명 주기 인식 코루틴 정지
CoroutineScope
를 사용하면 적절하게 장기 실행 작업을 자동으로 취소할 수 있지만, Lifecycle
이 특정 상태에 있지 않다면 코드 블록의 실행을 정지하려는 다른 경우가 있을 수 있습니다. 예를 들어 FragmentTransaction
을 실행하려면 Lifecycle
이 적어도 STARTED
상태가 될 때까지 기다려야 합니다. 이러한 상황을 위해 Lifecycle
은 lifecycle.whenCreated
, lifecycle.whenStarted
및 lifecycle.whenResumed
와 같은 추가 메서드를 제공합니다. 이러한 블록 내부에서 실행되는 코루틴은 Lifecycle
이 원하는 최소한의 상태가 아니면 정지됩니다.
아래 예에는 관련 Lifecycle
이 적어도 STARTED
상태일 때만 실행되는 코드 블록이 포함되어 있습니다.
class MyFragment: Fragment { init { // Notice that we can safely launch in the constructor of the Fragment. lifecycleScope.launch { whenStarted { // The block inside will run only when Lifecycle is at least STARTED. // It will start executing when fragment is started and // can call other suspend methods. loadingView.visibility = View.VISIBLE val canAccess = withContext(Dispatchers.IO) { checkUserAccess() } // When checkUserAccess returns, the next line is automatically // suspended if the Lifecycle is not *at least* STARTED. // We could safely run fragment transactions because we know the // code won't run unless the lifecycle is at least STARTED. loadingView.visibility = View.GONE if (canAccess == false) { findNavController().popBackStack() } else { showContent() } } // This line runs only after the whenStarted block above has completed. } } }
코루틴이 활성 상태인 동안 when
메서드 중 하나를 통해 Lifecycle
이 끝나면 코루틴은 자동으로 취소됩니다. 아래 예에서는 Lifecycle
상태가 DESTROYED
이면 finally
블록이 실행됩니다.
class MyFragment: Fragment { init { lifecycleScope.launchWhenStarted { try { // Call some suspend functions. } finally { // This line might execute after Lifecycle is DESTROYED. if (lifecycle.state >= STARTED) { // Here, since we've checked, it is safe to run any // Fragment transactions. } } } } }
LiveData와 함께 코루틴 사용
LiveData
를 사용할 때 값을 비동기적으로 계산해야 할 수 있습니다. 예를 들어 사용자의 환경설정을 검색하여 UI에 제공하려고 할 수 있습니다. 이러한 경우 liveData
빌더 함수를 사용해 suspend
함수를 호출하여 결과를 LiveData
객체로 제공할 수 있습니다.
아래 예에서 loadUser()
는 다른 곳에서 선언된 정지 함수입니다. liveData
빌더 함수를 사용하여 loadUser()
를 비동기적으로 호출한 후 emit()
를 사용하여 결과를 내보냅니다.
val user: LiveData<User> = liveData { val data = database.loadUser() // loadUser is a suspend function. emit(data) }
liveData
빌딩 블록은 코루틴과 LiveData
간에 구조화된 동시 실행 프리미티브 역할을 합니다. 코드 블록은 LiveData
가 활성화되면 실행을 시작하고 LiveData
가 비활성화되면 구성 가능한 제한 시간 후 자동으로 취소됩니다. 코드 블록이 완료 전에 취소되는 경우 LiveData
가 다시 활성화되면 다시 시작됩니다. 이전 실행에서 성공적으로 완료되었다면 다시 시작되지 않습니다. 자동으로 취소되었을 때만 다시 시작됩니다. 블록이 다른 이유로 취소되었다면(예: CancellationException
발생) 다시 시작되지 않습니다.
블록에서 여러 값을 내보낼 수도 있습니다. 각 emit()
호출은 LiveData
값이 기본 스레드에서 설정될 때까지 블록의 실행을 정지합니다.
val user: LiveData<Result> = liveData { emit(Result.loading()) try { emit(Result.success(fetchUser())) } catch(ioException: Exception) { emit(Result.error(ioException)) } }
다음 예와 같이 liveData
를 Transformations
와 결합할 수도 있습니다.
class MyViewModel: ViewModel() { private val userId: LiveData<String> = MutableLiveData() val user = userId.switchMap { id -> liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) { emit(database.loadUserById(id)) } } }
새 값을 내보내려고 할 때는 언제든지 emitSource()
함수를 호출하여 LiveData
에서 여러 값을 내보낼 수 있습니다. emit()
또는 emitSource()
를 호출할 때마다 이전에 추가한 소스가 삭제됩니다.
class UserDao: Dao { @Query("SELECT * FROM User WHERE id = :id") fun getUser(id: String): LiveData<User> } class MyRepository { fun getUser(id: String) = liveData<User> { val disposable = emitSource( userDao.getUser(id).map { Result.loading(it) } ) try { val user = webservice.fetchUser(id) // Stop the previous emission to avoid dispatching the updated user // as `loading`. disposable.dispose() // Update the database. userDao.insert(user) // Re-establish the emission with success type. emitSource( userDao.getUser(id).map { Result.success(it) } ) } catch(exception: IOException) { // Any call to `emit` disposes the previous one automatically so we don't // need to dispose it here as we didn't get an updated value. emitSource( userDao.getUser(id).map { Result.error(exception, it) } ) } } }
코루틴에 관한 자세한 내용은 다음 링크를 참고하세요.
추가 리소스
수명 주기 인식 구성요소와 함께 코루틴을 사용하는 방법을 자세히 알아보려면 다음 리소스를 확인하세요.
샘플
블로그
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- LiveData 개요
- 수명 주기 인식 구성요소로 수명 주기 처리
- 페이징 데이터 로드 및 표시