CoroutineScope는 CoroutineContext를 유일한 프로퍼티로 가지고 있는 인터페이스이다.
interface CoroutineScope {
val coroutineContext: CoroutineContext
}
CoroutineScope 인터페이스를 구현한 클래스를 만들고 내부에서 코루틴 빌더를 직접 호출할 수 있다.
class SomeClass : CoroutineScope {
override val coroutineContext: CoroutineContext = Job()
fun onStart() {
launch {
// ...
}
}
}
하지만 이런 방법은 CoroutineScope를 구현한 클래스에서 cancel이나 ensureActive 같은 다른 CoroutineScope의 메서드를 직접 호출하면 문제가 발생할 수 있다.
대신, 코루틴 스코프 인스턴스를 프로퍼티로 가지고 있다가 코루틴 빌더를 호출할 때 사용하는 방법이 선호된다.
class SomeClass {
val scope: CoroutineScope = ...
fun onStart() {
scope.launch {
// ...
}
}
}
코루틴 스코프 객체를 만드는 가장 쉬운 방법은 CoroutineScope 팩토리 함수를 사용하는 것이다. 이 함수는 컨텍스트를 넘겨받아 스코프를 만든다. (Job이 컨텍스트에 없으면 구조화된 동시성을 위해 Job을 추가할 수도 있다.)
public fun CoroutineScope(
context: CoroutineContext
): CoroutineScope =
ContextScope(
if (context[Job] != null) context
else context + Job()
)
internal class ContextScope(
context: CoroutineContext
) : CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun toString(): String =
"CoroutineScope(coroutineContext=$coroutineContext)"
}
abstract class BaseViewModel : ViewModel() {
protected val scope = CoroutineScope(TODO())
}
class MainViewModel(
private val userRepo: UserRepository,
private val newsRepo: NewsRepository,
) : BaseViewModel() {
fun onCreate() {
scope.launch {
val user = userRepo.getUser()
view.showUserData(user)
}
scope.launch {
val news = newsRepo.getNews()
.sortedByDescending { it.date }
view.showNews(news)
}
}
}
BaseViewModel에서 스코프를 만들면, 모든 ViewModel에서 쓰일 스코프를 단 한번으로 정의할 수 있다.
abstract class BaseViewModel : ViewModel() {
protected val scope = CoroutineScope(Dispatchers.Main)
}
안드로이드에서는 메인 스레드가 많은 수의 함수를 호출해야 하므로 기본 디스패처를 Dispatchers.Main으로 하는 것이 좋다.
abstract class BaseViewModel : ViewModel() {
protected val scope = CoroutineScope(Dispatchers.Main)
}
다음으로는 스코프를 취소 가능하게 만들어야 한다. 일반적으로 사용자가 스크린을 나가면 onDestroy 메서드(ViewModel에서는 onCleared)를 호출하면서 진행 중인 모든 작업을 취소한다.
스코프를 취소 가능하게 하려면 Job이 필요하다.(사실 CoroutineScope 함수가 Job을 추가하므로 따로 추가하지 않아도 상관은 없다).
abstract class BaseViewModel : ViewModel() {
protected val scope =
CoroutineScope(Dispatchers.Main + Job())
override fun onCleared() {
scope.coroutineContext.cancelChildren()
}
}
전체 스코프 대신 스코프가 가지고 있는 자식 코루틴만 취소하는 것이 더 좋은 방법이다. 같은 스코프에서 새로운 코루틴을 시작할 수 있다.
abstract class BaseViewModel : ViewModel() {
protected val scope =
CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onCleared() {
scope.coroutineContext.cancelChildren()
}
}
코루틴이 독립적으로 작동하려면 Job 대신 SupervisorJob을 사용해야 한다.
abstract class BaseViewModel : ViewModel() {
private val _failure: MutableLiveData<Throwable> =
MutableLiveData()
val failure: LiveData<Throwable> = _failure
private val exceptionHandler =
CoroutineExceptionHandler { _, throwable ->
_failure.value = throwable
}
private val context =
Dispatchers.Main + SupervisorJob() + exceptionHandler
protected val scope = CoroutineScope(context)
override fun onCleared() {
context.cancelChildren()
}
}
마지막으로 중요한 기능은 잡히지 않은 예외를 처리하는 것이다.
지금은 안드로이드에서 스코프를 따로 정의하는 대신에 viewModelScope 또는 lifecycleScope를 사용한다.
public val ViewModel.viewModelScope: CoroutineScope
get() = synchronized(VIEW_MODEL_SCOPE_LOCK) {
getCloseable(VIEW_MODEL_SCOPE_KEY)
?: createViewModelScope().also { scope -> addCloseable(VIEW_MODEL_SCOPE_KEY, scope) }
}
private val VIEW_MODEL_SCOPE_LOCK = SynchronizedObject()
internal fun createViewModelScope(): CloseableCoroutineScope {
val dispatcher = try {
Dispatchers.Main.immediate
} catch (_: NotImplementedError) {
EmptyCoroutineContext
} catch (_: IllegalStateException) {
EmptyCoroutineContext
}
return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob())
}
Dispatchers.Main과 SupervisorJob을 사용하고, 뷰 모델이나 라이프사이클이 종료되었을 때 잡을 취소시킨다는 점에서 위에서 만들었던 스코프와 거의 동일하다고 볼 수 있다.
스코프에서 특정 컨텍스트가 필요 없다면 viewModelScope와 lifecycleScope를 사용하는 것이 편리하고 더 좋다.