Launch one Coroutine at a time - Kotlin

Viewed 401

We have a button in the UI, which, when pressed, will make some remote network call in its own coroutine. However, if the user spams the button for whatever reason, it is possible that the remote data might somehow get corrupted. We would like to prevent this by discarding all requests until the current one is completed.

There are many ways to do this. I have create a simple extension function on CoroutineScope to only launch if the CoroutineScope is not active. This is what I have created:

Extension Function

fun CoroutineScope.safeLaunch(dispatcher: CoroutineDispatcher, block: () -> Unit): Job {
    return if (!isActive) {
        launch(dispatcher) {
            block()
        }
    } else {
        launch {}
    }
}

Example Use

fun loadNotifications() {
    viewModelScope.safeLaunch(IO) {
        getNotifications.invoke() // Suspend function invoke should only be from a coroutine or another suspend function
    }
}

The problem is, the above won't compile as I get an error saying

Suspend function invoke should only be from a coroutine or another suspend function

Does anyone know what I'm doing wrong or how to make it work?

1 Answers

There are multiple problems with this code:

  1. Fixing the error you mentioned is very easy and requires to only specify block as suspendable: block: suspend () -> Unit.
  2. isActive doesn't mean the job/scope is actively running something, but that it hasn't finished. isActive in your example always returns true, even before launching any coroutine on it.
  3. If your server can't handle concurrent actions, then you should really fix this on server side. Limiting the client isn't a proper fix as it can be still exploited by users. Also, you need to remember that multiple clients can perform the same action at the same time.

As you mentioned, there are several ways how this situation can be handled on the client side:

  1. In the case of UI and the button, it is probably the best for the user experience to disable the button or overlay the screen/button with a loading indicator. It gives the user the feedback that the operation is running in the background and at the same time it fixes the problem with multiple calls to the server.

  2. In general case, if we just need to limit concurrency and reject any additional tasks while the last one is still running, probably the easiest is to use Mutex:

private val scope = CoroutineScope(EmptyCoroutineContext)
private val mutex = Mutex()

fun safeLaunch(block: suspend () -> Unit) {
    if (!mutex.tryLock()) {
        return
    }

    scope.launch {
        try {
            block()
        } finally {
            mutex.unlock()
        }
    }
}

Note we need a separate mutex per scope or per the type of the task. I don't think it is possible to create such utility as a generic extension function, working with any coroutine scope. Actually, we can implement it in a very similar way to your original code, but by looking at the current job's children. Still, I consider such solution hacking and I discourage it.

Related