Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scope confused in coroutines

I have a use case which I want to use coroutine but a little confused how to implement it.

A ViewModel which has a scope and bind to the UI lifecycle and call an API from the repository:

class UserViewModel(): CoroutineScope {

    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    fun showUser() { 
       launch {
          val user = repo.getUser() 
          livedata = user
       }
    }

    fun onClean() {
       job.cancel()
    }
}

The repository use coroutine to build the network call like this:

suspend fun getUser() = GlobalScope { ... }

The use case is the repository function need to be always fully executed once the API is called from ViewModel since we need to capture all the network response from the server.

How I can make sure the coroutine in the repository is always executed but the ViewModel coroutines will be canceled to avoid memory leak once view model is cleared?

like image 219
iammini Avatar asked Nov 25 '18 22:11

iammini


2 Answers

According to the documentation of the GlobalScope I think we can rely that coroutine, launched using the global CoroutineScope, is always executed. The documentation says:

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely.

I've implemented some test code, and when the job was canceled inside the UserViewModel the coroutine in repository continued executing. Here is the code with my comments:

class UserViewModel(): CoroutineScope {
    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    fun showUser() {
        launch {
            val repo = Repository()
            val userDeferred = repo.getUser()
            // if onClean() is called before the coroutine in Repository finishes,
            // this line will not be called, but coroutine in Repository will continue executing
            val result = userDeferred.await() // wait for result of I/O operation without blocking the main thread
        }
    }

    fun onClean() {
        job.cancel()
    }
}

class Repository {
    fun getUser() = GlobalScope.async {
        delay(4000)
        // this line is executed no matter whether the job in UserViewModel was canceled or not
        "User returned"
    }
}

Additionally we can reduce showUser() function:

fun showUser() = repo.getUser().then(this) {
    // `it` contains the result
    // here is the main thread, use `it` to update UI
}

using extension function then:

fun <T> Deferred<T>.then(scope: CoroutineScope = GlobalScope, uiFun: (T) -> Unit) {
    scope.launch { uiFun([email protected]()) }
}

If you develop for Android and want to be sure your IO operation is executed completely even after cleaning up the ViewModel, use WorkManager. It is intended for asynchronous and deferrable tasks that require a guarantee that the system will run them even if the app exits.

like image 93
Sergey Avatar answered Oct 01 '22 04:10

Sergey


ViewModel only survives configuration changes and doesn't survive the destruction of activity in general.

If you want your operation to continue beyond the destruction of activity you should use a component with a lifecycle beyond that of activity i.e Service.

Furthermore if you want to make sure your operation is "always" executed you should use a Foreground service which will need to have an undismissable notification while the service is running.

A started service can use the startForeground(int, Notification) API to put the service in a foreground state, where the system considers it to be something the user is actively aware of and thus not a candidate for killing when low on memory.

like image 40
saiedmomen Avatar answered Oct 01 '22 03:10

saiedmomen