Ankit Dubey
Ankit Dubey

Reputation: 1170

Coroutine never calls API after once API is failed

In my viewmodel i've a function which uses coroutine to call an API.

fun loadPosts(){

    GlobalScope.launch(coroutineContext){
        changeState(load = true)
        val list= withContext(Dispatchers.IO) {
            apiService.getProfile(AUTH)
        }
        changeState(false,false,null)
        showResult(list)
    }
}

Everytime i click on a button, this function is fired, API is called and I get valid response. But once my api get Exception like 500 or Http 401 Unauthorized, then again when I hit the button, Coroutine is never called and seems it returns the again error message from cache.

For a use case:

I clicked on button -> Api is called -> got success response

Again I clicked on -> Api is called -> got success response

I disconnected my internet from phone

I clicked on button -> Api is called -> got exception something like ConnectionError

I connected my phone to internet

I clicked on button -> Api is not called -> got exception something like ConnectionError

Now even my phone has valid internet connection, I press on button, instead of calling api and wait for response, It gives me previous failed response again and again.

Earlier I was using Rxjava and I didn't face any such issue in that. I am new to coroutines so if anyone have any suggestion you are most welcome

Upvotes: 3

Views: 1132

Answers (2)

Choim
Choim

Reputation: 392

One more option.

If you use androidx.lifecycle:lifecycle-extensions:2.1.0, ViewModel now has viewModelScope extension property use SupervisorJob default. And it will be cleared automatically when ViewModel cleared.

Upvotes: 0

zsmb13
zsmb13

Reputation: 89528

Whenever you start a coroutine with a coroutine builder such as launch, you need to start it in a given CoroutineScope - this is enforced by the function being defined as an extension on CoroutineScope. This scope contains a CoroutineContext, which will define how the coroutine is executed.

Based on the comment above, I assume you are using roughly this setup:

abstract class BaseViewModel : ViewModel(), CoroutineScope { 
    private val job: Job = Job()
    override val coroutineContext: CoroutineContext 
        get() = Dispatchers.Main + job

    override fun onCleared() {
        coroutineContext.cancel()
    }
}

By using GlobalScope.launch(coroutineContext), you're actually overriding everything provided by GlobalScope with the context parameter. Plus, since your ViewModel itself is a scope already, you don't need to launch in GlobalScope in the first place. You can simply write down launch within the ViewModel with no scope specified (essentially this.launch {}) and no context passed to it, as it will get the one from the scope anyway.

The other issue is that you're using a regular Job as a part of your CoroutineContext. This Job becomes a parent for every coroutine you start, and whenever a child coroutine fails, such as on a network error, the parent Job gets cancelled too - meaning that any further children you attempt to start will fail immediately as well, as you can't start a new child under an already failed Job (see the Job documentation for more details).

To avoid this, you can use a SupervisorJob instead, which can also group your coroutines together as children, and cancel them when the ViewModel is cleared, but won't get cancelled if one of its children fail.


So to sum up the fixes to make at the code level quickly:

  • Use a SupervisorJob in the ViewModel (and while you're there, create this combined CoroutineContext just once, by directly assigning it, and not placing it in a getter):

    override val coroutineContext: CoroutineContext = Dispatchers.Main + SupervisorJob()
    
  • Launch your coroutines in the scope defined in the ViewModel instead of in GlobalScope:

    abstract class BaseViewModel : ViewModel(), CoroutineScope { 
         // ...
    
        fun load() {
            launch { // equivalent to this.launch, because `this` is a CoroutineScope
                // do loading
            }
        }
    }
    

You may also want to consider having your ViewModel contain a CoroutineScope instead of implementing the interface itself, as described here an discussed here.


There's a lot of reading to do on this topic, here are some articles I usually recommend:

... and everything else by Roman Elizarov on his blog, really :)

Upvotes: 3

Related Questions