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 개요
- 수명 주기 인식 구성요소로 수명 주기 처리
- 페이징 데이터 로드 및 표시