How to change coroutine scope?

Say, you have to make as suspending HTTP call in your ViewModel and based on that response execute an action: (Boolean)->Unit.
Now, action() need to be executed in the Activity/Fragment lifecicleScope so i would do something like:

// we are into the ViewModel
fun makeCall(
    lifecycleScope: AndroidLifecycleScope, 
    action: (Boolean) -> Unit
) = viewModelScope.launch {
  try {
    val result = httpClinet.get<MyClass>(myUrl)
    lifecycleScope.launch { action(true) }
  } catch (e: Throwable) {
    lifecycleScope.launch { action(false) }
  }
}

The problem here is that i need to catch the error inside makeCall() since the error does not get propagated.
Also, is this the correct way to handle this usecase? I am pretty sure I am missing something.

Should I post the error as well as a MutableLiveData<Throwable> and observe it?

First, why does action() need to run in a scope? It’s not a suspend function.

Second, passing a callback doesn’t seem very coroutine-y.

Third, this doesn’t seem very MVVM-y. The Activity/Fragment should observe some property in the view model to know when the call completes. The data in the property should be relevant to what the view needs to display. The view doesn’t care what exception was thrown, just that there was a failure.

Maybe the property is a sealed class, with one subclass for “processing” (view displays a progress spinner), one for success which contains the result, and one for failure which contains an error message to display to the user.

How about:

fun makeCall(
    lifecycleScope: AndroidLifecycleScope, 
    action: (Boolean) -> Unit
) = lifecycleScope.launch(start = CoroutineStart.UNDISPATHCED) {
  try {
    withContext(viewModelScope.coroutineContext) {
        httpClinet.get<MyClass>(myUrl)
    }
    action(true)
  } catch (e: Throwable) {
    action(false)
  }
}

This will call withContext in the calling thread, which will schedule httpClient in the view model context, and resume from withContext in the life cycle context.

Yeah I forgot to add the suspend modifier to action.
Indeed I am a little ashamed of this solution.

In my app, what I am trying to achieve is to wait for the login call, then if it ended without error, let the activity know it. The possible errors are sealed classes. I suppose I should catch the error and post either a Boolean or the error.

The reason behind why I was using the lifecycleScope was to avoid to call action if the owner was destroyed. Indeed observing the error as MutableLiveData<AuthException> is more elegant and Android friendly.

But still, what’s the idiomatic way of changing scope?

In the world of structured concurrency, launching a coroutine in another scope is sort of like goto, which is evil :wink:. So I don’t think you should do it.

Conceptually, the only thing that should launch coroutines on a scope is the thing that owns the scope. The Activity has a scope, so only the Activity should launch on the scope.

So I am not approaching the problem correctly?

The real question is, “What problem are you trying to solve?” We’ve already got a solution for your original problem that doesn’t involve changing scope. So what’s a problem whose solution might involve changing scope?

1 Like

Good point

@mtimmerm I think that code will do one of two things:

  1. The withContext will crash because you are mixing contexts from two different scopes.
  2. It will switch execution to the dispatcher used by the viewModelScope, but it’s still running in the lifecycleScope, so it will be cancelled if the lifecycleScope is cancelled, which isn’t what we want.