Language/코루틴

Architecture components와 코루틴 Scope

728x90
반응형

CoroutineScope는 모든 Coroutine을 추적하고 Coroutine이 실행되어야 하는시기를 관리하는 데 도움이됩니다. 또한 그 안에서 시작된 모든 Coroutine을 취소 할 수 있습니다. 각 비동기 작업 또는 Coroutine은 특정 CoroutineScope 내에서 실행됩니다.

Architecture components는 앱의 논리적 범위에 대한 Coroutine에 대한 최고 수준의 지원을 제공합니다.

Architecture components는 앱에서 사용할 수 있는 다음과 같은 기본 제공 범위를 정의합니다.

1) ViewModelScope , 2) LifecycleScope, 3) LiveData

 

이 3가지 기본 Coroutine Scope는 각 해당 Architecture components에 대한 KTX extensions 에 있습니다.

이러한 범위를 사용할 때 적절한 종속성을 추가해야 합니다. 

 

 

KTX 종속성 추가

 

1) ViewModelScope :  androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0 이상을 사용합니다.

2) LifecycleScope : androidx.lifecycle:lifecycle-runtime-ktx:2.2.0 이상을 사용합니다.

3) liveData : androidx.lifecycle:lifecycle-livedata-ktx:2.2.0 이상을 사용합니다

 

 

 

 


수명주기-인식 코루틴 범위 : ViewModelScope, LifecycleScope

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

 

 

 

1) ViewModelScope


ViewModelScope는 앱의 각 ViewModel을 대상으로 정의됩니다.

이 범위에서 시작된 모든 코루틴은 ViewModel이 삭제되면 자동으로 취소됩니다.

코루틴은 ViewModel이 활성 상태인 경우에만 실행해야 할 작업이 있을 때 유용합니다.

예를 들어 레이아웃의 일부 데이터를 계산한다면 작업의 범위를 ViewModel로 지정하여 ViewModel을 삭제하면 리소스를 소모하지 않도록 작업이 자동으로 취소됩니다.

 

 

 

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

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // ViewModel이 지워지면, 취소되는 코루틴!!!
        }
    }
}

 

 

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

        // lifecycleScope에서 새 코루틴 만들기 
        viewLifecycleOwner.lifecycleScope.launch {
        
            // rrepeatOnLifecycle은 수명주기가
            // STARTED 상태 (or 이상)일 때마다 새 코루틴 블록을 시작하고,
            // STOPPED 일 때 취소합니다.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            
                // 흐름을 트리거하고 값 수신을 시작합니다.
                // 이는 수명주기가 시작될 때 발생하고, 수명주기가 중지되면 수집을 중지합니다.
                viewModel.someDataFlow.collect {
                    // 프로세스 항목
                }
            }
        }
    }
}

 

 

수명주기-인식 코루틴 정지

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

 

 

 


 

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

 

 

 

 

 

728x90
반응형