아직 글의 완성이 덜 되었습니다.
@SinceKotlin("1.3")
public interface CoroutineContext {
/**
* Returns the element with the given [key] from this context or `null`.
*/
public operator fun <E : Element> get(key: Key<E>): E?
/**
* Accumulates entries of this context starting with [initial] value and applying [operation]
* from left to right to current accumulator value and each element of this context.
*/
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
/**
* Returns a context containing elements from this context and elements from other [context].
* The elements from this context with the same key as in the other one are dropped.
*/
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
/**
* Returns a context containing elements from this context, but without an element with
* the specified [key].
*/
public fun minusKey(key: Key<*>): CoroutineContext
/**
* Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
*/
public interface Key<E : Element>
/**
* An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
*/
public interface Element : CoroutineContext {
/**
* A key of this coroutine context element.
*/
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
- get() : 연산자(operator) 함수로써 주어진 key 에 해당하는 컨텍스트 요소를 반환합니다.
- fold() : 초기값(initialValue)을 시작으로 제공된 병합 함수를 이용하여 대상 컨텍스트 요소들을 병합한 후 결과를 반환합니다. 예를들어 초기값을 0을 주고 특정 컨텍스트 요소들만 찾는 병합 함수(filter 역할)를 주면 찾은 개수를 반환할 수 있고, 초기값을 EmptyCoroutineContext 를 주고 특정 컨텍스트 요소들만 찾아 추가하는 함수를 주면 해당 요소들만드로 구성된 코루틴 컨텍스트를 만들 수 있습니다.
- plus() : 현재 컨텍스트와 파라미터로 주어진 다른 컨텍스트가 갖는 요소들을 모두 포함하는 컨텍스트를 반환합니다. 현재 컨텍스트 요소 중 파라미터로 주어진 요소에 이미 존재하는 요소(중복)는 버려집니다.
- minusKey() : 현재 컨텍스트에서 주어진 키를 갖는 요소들을 제외한 새로운 컨텍스트를 반환합니다.
위 이미지는 우리가 GlobalScope.launch{} 를 수행할 때 launch 함수의 첫번째 파라미터인 CoroutineContext 에 어떤 값을 넘기는지에 따라서 변화되어 가는 코루틴 컨텍스트의 상태를 보여줍니다.
각각의 요소를 + 연산자를 이용해 연결하고 있는데 이는 앞서 설명한 것처럼 CoroutineContext가 plus 연산자를 구현하고 있기 때문입니다. Element + Element + … 는 결국 하나로 병합 된 CoroutineContext (e.g. CombinedContext)를 만들어냅니다.
CoroutineScope는 CoroutineContext 하나만 멤버 변수로 정의하고 있는 인터페이스 입니다.
1. CoroutineScope
2. GlobalScope
GlobalScope은 싱글톤으로 생성되기 때문에, 잘못 활용한다면 프로그램 전체에 악영향을 미칠 수 있습니다.
1. ViewModelScope
AAC의 Lifecycle Component 라이브러리의 ViewModel 내부에서는 viewModelScope 라는 코루틴 스코프를 이용할 수 있습니다. 그래서 CoroutineScope의 객체를 직접 만들어서 사용하는 것은 안드로이드 개발에서 잘 하지 않습니다. viewModelScope는 onCleared()에 자동으로 작업을 취소시켜주는 역할을 합니다. 그래서 뷰모델의 생명주기에 코루틴 작업을 맞출 수 있습니다.
viewModelScope 내부 입니다. Scope의 context는 Dispatchers.Main 입니다.
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
구글 공식문서의 예로는 위와 같이 사용이 가능합니다.
class MyViewModel: ViewModel() {
fun MainThread() {
viewModelScope.launch {
liveData.value = getDataFromNetwork()
}
}
fun WorkerThread() {
viewModelScope.launch {
withContext(Dispatchers.Main) {
liveData.value = getDataFromNetwork()
}
}
}
suspend fun getDataIO() : Int = withContext(Dispatchers.IO) {
kotlinx.coroutines.delay(5000)
}
}
setValue()는 메인 쓰레드에서 LiveData의 값을 변경합니다. 메인 쓰레드에서 바로 값을 변경해주기 때문에 setValue() 함수를 호출한 뒤 바로 밑에서 getValue() 함수로 값을 읽어오면 변경된 값을 가져올 수 있습니다.
setValue()는 메인 쓰레드에서 값을 dispatch 하기 때문에 백그라운드에서 setValue()를 호출한다면 에러가 납니다. 위의 viewModelScope는 자동으로 Dispatchers.Main에서 시작하기 때문에 LiveData의 setValue()를 사용할 수 있습니다. 그게 아닌 경우에는 withContext로 Context 스위칭을 해야합니다.
2. LifecycleScope
LifecycleScope는 각 Lifecycle 객체에서 정의됩니다. 이 범위에서 실행된 코루틴은 Lifecycle이 끝날 때 제거됩니다. lifecycle.coroutineScope 또는 lifecycleOwner.lifecycleScope 속성을 통해 Lifecycle의 CoroutineScope에 액세스할 수 있습니다.
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)
}
}
}
위 예는 공식문서에서 lifecycleOwner.lifecycleScope를 사용하여 미리 계산된 텍스트를 비동기적으로 만드는 방법을 보여줍니다.
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)
}
https://developer.android.com/topic/libraries/architecture/coroutines
https://myungpyo.medium.com/reading-coroutine-official-guide-thoroughly-part-1-7ebb70a51910
https://silica.io/understanding-kotlin-coroutines/5/
https://thdev.tech/kotlin/2020/12/22/kotlin_effective_16/
https://medium.com/hongbeomi-dev/%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-%EC%BD%94%EB%A3%A8%ED%8B%B4-4-coroutine-context-and-dispatchers-1eab8f175428
https://kotlinlang.org/docs/coroutines-basics.html