class ViewModel : ViewModel() {
fun setSecondValue(value: Int) {
viewModelScope.launch {
dataStore.setSecondValue(value)
}
}
}
class dataStore(private val context: Context) {
suspend fun setSecondValue(value: Int) {
context.dataStore.edit { preferences ->
preferences[currentValueKey] = value
}
}
}
private fun setValues() {
val nextValue = (initValue.toInt() + 1).toString()
viewModel.setFirstValue(initValue)
viewModel.setSecondValue(nextValue)
goToSecondActivity()
}
setSecondValue
까지만 실행되고, suspend function인 dataStore.setCurrentValue(value)
는 실행되지 않음private suspend fun setValues() {
val job = CoroutineScope(Dispatchers.Main).launch {
val nextValue = (initValue.toInt() + 1).toString()
viewModel.setFirstValue(initValue)
viewModel.setSecondValue(nextValue)
}
job.join()
goToSecondActivity()
}
private fun goToSecondActivity() {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
this.dismiss()
}
원인 파악을 위해 로그를 찍어보기로 했다. 일단 뷰모델이 소멸될 때 onCleared()가 호출되기 때문에, onCleared()에 로그를 찍어서 언제 뷰모델이 소멸되는지 확인하기로 했다.
class ViewModel : ViewModel() {
fun setSecondValue(value: Int) {
viewModelScope.launch {
Log.e("Jinnie", "ViewModel $value")
dataStore.setSecondValue(value)
}
}
override fun onCleared() {
super.onCleared()
Log.e("Jinnie", "onCleared()")
}
}
class dataStore(private val context: Context) {
suspend fun setSecondValue(value: Int) {
context.dataStore.edit { preferences ->
Log.e("Jinnie", "DataStore $value")
preferences[currentValueKey] = value
}
}
}
private fun setValues() {
val nextValue = (initValue.toInt() + 1).toString()
viewModel.setFirstValue(initValue)
viewModel.setSecondValue(nextValue)
goToSecondActivity()
}
private fun goToSecondActivity() {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
this.dismiss()
}
override fun onDestroy() {
super.onDestroy()
Log.e("Jinnie", "onDestroy()")
}
---> 다이얼로그 프래그먼트에서 버튼 클릭 시 goToSecondActivity 실행
2023-06-06 13:51:31.639 7914-7914 Jinnie com.example.mlkitproject E ViewModel 0
---> dataStore의 setCurrentValue가 실행되기 전에 뷰모델이 소멸됨
2023-06-06 13:51:31.994 7914-7914 Jinnie com.example.mlkitproject E onCleared
---> 프래그먼트 소멸
2023-06-06 13:51:31.995 7914-7914 Jinnie com.example.mlkitproject E onDestroy()
→ 다이얼로그 프래그먼트에서 버튼을 클릭하면 setValues
가 호출되고, setValues
의 내부 코드가 순차적으로 실행되면서 goToSecondActivity
를 통해 다른 액티비티로 이동
viewModel.setSecondValue(nextValue)
는 정상적으로 실행되지만, viewModelScope내의 dataStore.setSecondValue(value)
가 실행되기 전에 뷰모델이 소멸됨 viewModel.setValue(nextValue)
지 데이터스토어의 suspend function이 아니므로 소용이 없던 것다만,
setValues
에서viewModel.setSecondValue(nextValue)
만 실행할 경우는 문제 없음. 여러 작업들을 순차적으로 실행하는 경우 나중에 실행되는 작업은 뷰모델 인스턴스 소멸에 영향을 받아 이러한 케이스가 발생하는 듯 하다...🤔
작업의 수를 줄이지 않고, 기존 코드를 고수하면서 뷰모델 내에서 실행되는 스코프가 뷰모델의 lifecycle에 영향을 받지 않으려면 어떻게 해야 할까?
위에서 던진 질문에 대한 답변은, viewModelScope가 아닌 CoroutineScope를 사용하되, 인자로 SupervisorJob
을 전달하는 것이다.
private val supervisorScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
일단 viewModelScope의 원형을 살펴보면 다음과 같다. ViewModelScope는 SupervisorJob을 사용해 스코프를 생성한다.
SupervisorJob 내의 자식 코루틴들은 서로에게 영향을 받지 않는다. 따라서 여러개의 자식 코루틴들 중 하나가 취소되어도 다른 코루틴에 영향을 주지 않으며, SupervisorJob에도 영향을 미치지 않는다.
A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children, so a supervisor can implement a custom policy for handling failures of its children
따라서 viewModelScope의 자식 코루틴들 중 하나가 취소되어도 다른 자식 코루틴과 부모 코루틴엔 영향이 가지 않는다. 하지만 나의 경우엔 뷰모델이 소멸되면서 부모 스코프인 viewModelScope도 취소되었기 때문에 내부의 자식 코루틴들도 모두 취소되었던 것이다. 따라서 이 문제를 해결하기 위해선 뷰모델의 라이프사이클에 영향을 받지 않는 독립적인 CoroutineScope를 사용해야 했다.
일단 CoroutineScope의 원형을 살펴보면, 파라미터로 context를 받는 것을 알 수 있다. 다만 context가 Job을 포함하지 않을 경우, 일반 Job이 생성된다. 그리고 해당 스코프에서 하나의 자식 코루틴이라도 실패하거나 취소될 경우, SupervisorJob과 다르게 다른 자식 코루틴들이 모두 취소된다.
If the given context does not contain a Job element, then a default Job() is created. This way, failure of any child coroutine in this scope or cancellation of the scope itself cancels all the scope's children, just like inside coroutineScope block.
CoroutineScope가 리턴하는 일반 Job의 원형도 살펴봤다. 자식 코루틴들이 서로에게 영향을 주지 않는 독립적인 작업을 만들고자 한다면 SupervisorJob을 사용하라고 명시되어 있다.
Creates a job object in an active state. A failure of any child of this job immediately causes this job to fail, too, and cancels the rest of its children.
To handle children failure independently of each other use SupervisorJob.
따라서 아래처럼 별도의 스코프를 정의해주니, 문제가 해결되었다.
class CountViewModel : ViewModel() {
private val dataStoreScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
fun setSecondValue(value: Int) {
dataStoreScope.launch {
dataStore.setSecondValue(value)
}
}
}
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html