Can rememberCoroutineScope and rememberStableCoroutineScope really replace viewModelScope in Circuit? #1883
-
I'm exploring the differences between AAC ViewModel's viewModelScope and Compose's rememberCoroutineScope, particularly in the context of using Circuit. I understand that when using Circuit, since we use Circuit Presenter instead of AAC ViewModel, we're supposed to use rememberCoroutineScope or rememberStableCoroutineScope to replace viewModelScope OrderServicesPresenter.kt in CatchUp class OrderServicesPresenter
@AssistedInject
constructor(
@Assisted private val navigator: Navigator,
private val serviceMetas: Map<String, ServiceMeta>,
private val catchUpPreferences: CatchUpPreferences,
) : Presenter<State> {
...
@Composable
override fun present(): State {
val storedOrder by
remember {
catchUpPreferences.servicesOrder.mapToStateFlow {
it ?: serviceMetas.keys.toImmutableList()
}
}
.collectAsState()
val initialOrderedServices =
remember(storedOrder) {
serviceMetas.values.sortedBy { storedOrder.indexOf(it.id) }.toImmutableList()
}
val currentDisplay =
remember(storedOrder) {
serviceMetas.values.sortedBy { storedOrder.indexOf(it.id) }.toMutableStateList()
}
val isChanged by remember {
derivedStateOf {
val initial = initialOrderedServices.joinToString { it.id }
val current = currentDisplay.joinToString { it.id }
initial != current
}
}
var showConfirmation by remember { mutableStateOf(false) }
BackHandler(enabled = isChanged && !showConfirmation) { showConfirmation = true }
val scope = rememberStableCoroutineScope()
return State(
services = currentDisplay,
showSave = isChanged,
showConfirmation = showConfirmation,
) { event ->
when (event) {
...
Save -> {
scope.launch {
save(currentDisplay)
navigator.pop()
}
}
is DismissConfirmation -> {
showConfirmation = false
if (event.save) {
scope.launch {
save(currentDisplay)
navigator.pop()
}
} else if (event.pop) {
navigator.pop()
}
}
...
}
}
} The viewModelScope in AAC ViewModel provides more than just a coroutine scope that terminates with the ViewModel's lifecycle: 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) }
} internal fun createViewModelScope(): CloseableCoroutineScope {
val dispatcher = try {
Dispatchers.Main.immediate
} catch (_: NotImplementedError) {
EmptyCoroutineContext
} catch (_: IllegalStateException) {
EmptyCoroutineContext
}
return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob())
} The key difference is that viewModelScope creates a coroutine scope with a In contrast, @Composable
inline fun rememberCoroutineScope(
crossinline getContext: @DisallowComposableCalls () -> CoroutineContext =
{ EmptyCoroutineContext }
): CoroutineScope {
val composer = currentComposer
val wrapper = remember {
CompositionScopedCoroutineScopeCanceller(
createCompositionCoroutineScope(getContext(), composer)
)
}
return wrapper.coroutineScope
} @PublishedApi
internal class CompositionScopedCoroutineScopeCanceller(
val coroutineScope: CoroutineScope
) : RememberObserver {
override fun onRemembered() {
// Nothing to do
}
override fun onForgotten() {
coroutineScope.cancel(LeftCompositionCancellationException())
}
override fun onAbandoned() {
coroutineScope.cancel(LeftCompositionCancellationException())
}
} If we want viewModelScope-like stability in Circuit, we might need to consider explicitly using SupervisorJob when creating coroutine scopes. However, I'd like to learn more about this before making a final decision. val scope = rememberCoroutineScope {
SupervisorJob() + Dispatchers.Main.immediate
} Questions: Is this understanding of the differences between viewModelScope and rememberCoroutineScope correct? Looking forward to community insights and discussions on this topic. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
First, I’d like to emphasize that the following reflects my personal opinion. To get straight to the point, if you need a However, I personally don’t think this is the best way to use it. Even if Moreover, adding a In light of these considerations, whether the coroutine uses a However, as I mentioned, if you personally need a coroutine with certain settings, you’re free to create and use it. |
Beta Was this translation helpful? Give feedback.
First, I’d like to emphasize that the following reflects my personal opinion.
To get straight to the point, if you need a
CoroutineScope
configured with a SupervisorJob, you can create it in the same wayrememberCoroutineScope
does.However, I personally don’t think this is the best way to use it.
Even if
viewModelScope
is created with aSupervisorJob
, if you don’t explicitly specify aCoroutineExceptionHandler
, any unhandled exception thrown by a coroutine in theviewModelScope
will triggerThread.uncaughtExceptionHandler
, which will crash the app.Moreover, adding a
CoroutineExceptionHandler
when launching a coroutine can lead to mistakes, as seen in this case. Therefore, for coroutine …