수명 주기 인식 구성요소로 Kotlin 코루틴 사용

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 속성을 통해 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)
        }
    }
}

재시작 가능한 수명 주기 인식 코루틴

LifecycleDESTROYED일 때 lifecycleScope가 장기 실행 작업을 자동으로 취소하는 올바른 방법을 제공하지만 Lifecycle이 특정 상태에 있을 때 코드 블록의 실행을 시작하고 다른 상태에 있을 때 취소하려는 경우가 있을 수 있습니다. 예를 들어 LifecycleSTARTED일 때 흐름을 수집하고 STOPPED일 때 수집을 취소하려고 할 수 있습니다. 이 방법은 UI가 화면에 표시될 때만 흐름 내보내기를 처리하여 리소스를 절약하고 앱 비정상 종료를 방지할 수 있습니다.

이러한 경우 LifecycleLifecycleOwner는 정확히 이를 실행하는 정지 repeatOnLifecycle API를 제공합니다. 다음 예에는 관련 Lifecycle이 최소 STARTED 상태일 때마다 실행되고, LifecycleSTOPPED일 때 취소되는 코드 블록이 포함되어 있습니다.

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 상태가 될 때까지 기다려야 합니다. 이러한 상황을 위해 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()는 다른 곳에서 선언된 정지 함수입니다. 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))
    }
}

다음 예와 같이 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)
                }
            )
        }
    }
}

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

추가 리소스

수명 주기 인식 구성요소와 함께 코루틴을 사용하는 방법을 자세히 알아보려면 다음 리소스를 확인하세요.

샘플

블로그