6월 3일의 ⁠#Android11: 베타 버전 출시 행사에 참여하세요.

아키텍처 구성요소와 함께 Kotlin 코루틴 사용

Kotlin 코루틴은 비동기 코드를 작성할 수 있게 하는 API를 제공합니다. Kotlin 코루틴을 사용하면 코루틴이 실행되어야 하는 시기를 관리하는 데 도움이 되는 CoroutineScope를 정의할 수 있습니다. 각 비동기 작업은 특정 범위 내에서 실행됩니다.

아키텍처 구성 요소LiveData와의 상호운용성 레이어는 물론 앱의 논리적 범위에 대한 코루틴을 가장 잘 지원합니다. 본 항목에서는 아키텍처 구성요소를 통해 코루틴을 효과적으로 사용하는 방법을 설명합니다.

KTX 종속성 추가

본 항목에서 설명하는 기본 제공 코루틴 범위는 각 아키텍처 구성요소의 KTX 확장에 포함되어 있습니다. 이러한 범위 사용 시 적절한 종속성을 추가해야 합니다.

  • ViewModelScope에서는 androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 이상을 사용하세요.
  • LifecycleScope에서는 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 이상을 사용하세요.
  • liveData에서는 androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 이상을 사용하세요.

수명 주기 인식 코루틴 범위

아키텍처 구성요소는 앱에 사용할 수 있는 다음 기본 제공 범위를 정의합니다.

ViewModelScope

ViewModelScope는 앱의 각 ViewModel에서 정의됩니다. 이 범위에서 시작된 모든 코루틴은 ViewModel이 삭제되면 자동으로 취소됩니다. 코루틴은 ViewModel이 활성 상태인 경우에만 실행할 작업이 있을 때 유용합니다. 예를 들어 레이아웃의 일부 데이터를 계산할 때에는 작업의 범위를 ViewModel로 지정해야 합니다. 그러면 ViewModel이 삭제될 때 작업이 자동으로 취소되어 리소스 소모를 방지할 수 있습니다.

다음 예에서와 같이 ViewModel의 viewModelScope 속성을 통해 ViewModelCoroutineScope에 액세스할 수 있습니다.

class MyViewModel: ViewModel() {
        init {
            viewModelScope.launch {
                // Coroutine that will be canceled when the ViewModel is cleared.
            }
        }
    }
    

LifecycleScope

LifecycleScope는 각 Lifecycle 개체에서 정의됩니다. 이 범위에서 시작된 모든 코루틴은 Lifecycle이 끝날 때 취소됩니다. lifecycle.coroutineScope 또는 lifecycleOwner.lifecycleScope 속성을 통해 LifecycleCoroutineScope에 액세스할 수 있습니다.

아래 예는 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)
            }
        }
    }
    

수명 주기 인식 코루틴 정지

CoroutineScope를 사용하면 적절하게 장기 실행 작업을 자동으로 취소할 수 있지만 개발자가 Lifecycle이 특정 상태에 있지 않다면 코드 블록의 실행을 정지하려는 경우도 있습니다. 예를 들어 FragmentTransaction을 실행하려면 Lifecycle이 적어도 STARTED 상태가 될 때까지 기다려야 합니다. 이러한 경우 Lifecyclelifecycle.whenCreated, lifecycle.whenStartedlifecycle.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()는 다른 곳에서 선언된 suspend 함수입니다. liveData 빌더 함수를 사용하여 loadUser()를 비동기적으로 호출한 후 emit()를 사용하여 결과를 내보냅니다.

val user: LiveData<User> = liveData {
        val data = database.loadUser() // loadUser is a suspend function.
        emit(data)
    }
    

liveData 빌딩 블록은 코루틴과 LiveData 간에 구조화된 동시 실행 프리미티브 역할을 합니다. 코드 블록은 LiveData가 활성화되면 실행을 시작하고 LiveData가 비활성화되면 구성 가능한 제한 시간 후 자동으로 취소됩니다. 코드 블록이 완료 전에 취소되는 경우 LiveData가 다시 활성화되면 다시 시작됩니다. 이전 실행에서 성공적으로 완료되었다면 다시 시작되지 않습니다. 자동으로 취소되었을 때에만 다시 시작됩니다. 블록이 다른 이유로 취소되었다면(예: CancelationException 발생) 다시 시작되지 않습니다.

블록에서 여러 값을 내보낼 수도 있습니다. 각 emit() 호출은 LiveData 값이 기본 스레드에서 설정될 때까지 블록의 실행을 정지합니다.

val user: LiveData<Result> = liveData {
        emit(Result.loading())
        try {
            emit(Result.success(fetchUser()))
        } catch(ioException: Exception) {
            emit(Result.error(ioException))
        }
    }
    

다음 예에서와 같이 liveDataTransformations와 결합할 수도 있습니다.

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)
                    }
                )
            }
        }
    }
    

코루틴에 관한 자세한 내용은 다음 링크를 참조하세요.