Home > Software design >  How to make coroutines launch next job only when first is finished
How to make coroutines launch next job only when first is finished

Time:05-20

Lets say that I have a button, which has a listener in Android:

someView.setOnClickListener {
    viewModel.doSomething()
}

This will create a new Job for each time that the button is pressed. In my ViewModel, the doSomething function is launching suspend functions from the viewModelScope:

fun doSomething() {
  viewModelScope.launch {
       doSomething1()
       doSomething2()
       doSomething3() 
   }
}

suspend fun doSomething1() {
    delay(100)
    Log.d("TEST", "doSomething1: 1 ")
}

suspend fun doSomething2() {
    delay(300)
    Log.d("TEST", "doSomething2: 2 ")
}

fun doSomething3() {
    Log.d("TEST", "doSomething3: 3 ")
}

Now, if this button gets pressed very quickly in succession (in theory, lets say i can call the function from the listener twice, so that the execution of the first call hasn't finished yet), I will get following result in my logcat:

D/TEST: doSomething1: 1
D/TEST: doSomething1: 1
D/TEST: doSomething2: 2
D/TEST: doSomething3: 3
D/TEST: doSomething2: 2
D/TEST: doSomething3: 3

What I actually want to achieve is, what would happen if I could launch doSomething() twice from the same scope, so it runs synchronous.

D/TEST: doSomething1: 1
D/TEST: doSomething1: 2
D/TEST: doSomething2: 3
D/TEST: doSomething3: 1
D/TEST: doSomething2: 2
D/TEST: doSomething3: 3

How can I achieve that behaviour, so that before starting the same coroutine, the first one has to finish?

CodePudding user response:

You can use a Mutex lock to do this - the lock would keep subsequent 1-2-3 sequences from launching until the prior ones are complete, no matter how many times you push the button.

private val lock = Mutex()

fun doSomething() {
  viewModelScope.launch {
       // lock means only one "1-2-3" sequence can execute 
       // at a time, subsequent calls will suspend here and wait
       // for the lock to be released before starting
       lock.withLock {
           doSomething1()
           doSomething2()
           doSomething3() 
       }
   }
}

You could also use a channel to run them one at a time

private val channel = Channel<Job>(capacity = Channel.UNLIMITED).apply {
    viewModelScope.launch {
        consumeEach { it.join() }
    }
}

fun doSomething() {
    channel.trySend(
        // send a lazily executed "1-2-3" job to the channel for it
        // to run (will run jobs one at a time and wait for each
        // job to complete before starting the next)
        viewModelScope.launch(start = CoroutineStart.LAZY) {
            doSomething1()
            doSomething2()
            doSomething3() 
        } 
    )
}

CodePudding user response:

Can be solved by saving the last job and waiting for it to finish before executing a new coroutine:

private var lastJob: Job? = null

fun doSomething() {
    val prevJob = lastJob
    val currentJob = lifecycleScope.launch {
        prevJob?.join()
        doSomething1()
        doSomething2()
        doSomething3()
    }
    lastJob = currentJob
}
  • Related