How to run a suspend function inside another one without waiting for its result?

5.2k Views Asked by At

I have a scenario where my code has to send an api call and move on with its work (which contains another api call) without waiting for the result of the first call.

Right now I do this in my viewmodel

fun showItem(id:Int) {
   launch{
       repo.markItemRead(id)
   }
   launch {
       try {
           val item = repo.getItemById(id).getOrThrow
           commands.postValue(ShowItemCommand(item))
       } catch (t:Throwable) {
           commands.postValue(ShowError(R.string.error_retrieve_item))
           repo.logError(t)
       }
   }
}

this calls the repository which has these two functions

suspend fun markItemRead(id) {
    try {
        service.markItemAsRead(id)
    } catch(ignored:Throwable) {
    }
}

suspend fun getItemById(id) : Result<ItemData> {
    return try {
       val response : ItemEntity = service.getItemById(id)
       val item  = response.toData()
       Result.Success(item)
    } catch (t:Throwable) {
        Result.Failure(t)
    }
}

I would prefer it if the repository did all those jobs because one has to follow the other every time.

Unfortunatelly when I try to do something like this in my repository:

suspend fun getItemById(id:Int) : Result<ItemData> {
    try {
        service.markItemAsRead(id)
    } catch(ignored:Throwable) {
    }
    return try {
       val response : ItemEntity = service.getItemById(id)
       val item  = response.toData()
       Result.Success(item)
    } catch (t:Throwable) {
        Result.Failure(t)
    }
}

It waits for the markItemAsRead function to finish before moving on

Other than defining a scope for the repository and putting the markItemAsRead call inside a launch (which I have read is incorrect to do inside a suspending function) is there another way of doing this inside the repository?

4

There are 4 best solutions below

0
On BEST ANSWER

You can use coroutineScope or supervisorScope in the repository, depending on your needs. Both functions are designed for parallel decomposition of work. These functions return as soon as the given block and all its children coroutines are completed.

When any child coroutine in coroutineScope fails, this scope fails and all the rest of the children are cancelled. Unlike coroutineScope, a failure of a child coroutine in supervisorScope does not cause this scope to fail and does not affect its other children, so a custom policy for handling failures of its children can be implemented.

Please choose what best suits your needs. Example of usage:

suspend fun getItemByIdAndMarkRead(id: Int) : Result<ItemData> = supervisorScope {
    launch {
        try {
            service.markItemAsRead(id)
        } catch(ignored:Throwable) { }
    }

    return@supervisorScope withContext(Dispatchers.Default) {
        try {
            val response : ItemEntity = service.getItemById(id)
            val item  = response.toData()
            Result.Success(item)
        } catch (t: Throwable) {
            Result.Failure(t)
        }
    }
}

service.markItemAsRead(id) and service.getItemById(id) will execute in parallel.

1
On

you want to do multiple tasks in parallel but return function when all tasks done. if i'm right.

you can use async/await. inside suspend function

val d1 = async { t1() }
val d2 = async { t2() }
d1.await()
d2.await()
// all tasks done

t1 and t2 will be run in parallel. when t1.await() calls it's gonna wait for result but t2 still running.

in your function you can change it like this:

suspend fun getItemById(id:Int) : Result<ItemData> = coroutineScope {
    val t1 = async {
        try {
            service.markItemAsRead(id)
        } catch(ignored:Throwable) {
            null
        }
    }
    val t2 = async {
        try {
            val response : ItemEntity = service.getItemById(id)
            val item = response.toData()
            Result.Success(item)
        } catch (t:Throwable) {
            Result.Failure(t)
        }
    }
    t1.await()
    return@coroutineScope t2.await()
}
4
On

See if the coroutineScope function does what you want:

Creates a CoroutineScope and calls the specified suspend block with this scope. The provided scope inherits its coroutineContext from the outer scope, but overrides the context's Job.

suspend fun getItemById(id:Int) : Result<ItemData> {
    coroutineScope {
        launch {
            try {
                service.markItemAsRead(id)
            } catch(ignored:Throwable) { }
        }
    }

    return try {
       val response : ItemEntity = service.getItemById(id)
       val item  = response.toData()
       Result.Success(item)
    } catch (t:Throwable) {
        Result.Failure(t)
    }
}
0
On

is there another way of doing this inside the repository?

No there isn't

(which I have read is incorrect to do inside a suspending function)

It is perfectly okay to use launch in a suspend function for a fire and forget task