Image, you have a Kotlin suspend function
suspend fun foo(): String { return "" }
I need to make it callback-based instead of suspend, i.e.
interface Cancellable {
fun cancel()
}
internal class CancellableImpl : Cancellable {
var cancellationCallback: (() -> Unit)? = null
override fun cancel() {
cancellationCallback?.invoke()
}
}
fun foo(callback: (String) -> Unit): Cancellable
Would GlobalScope approach work? I.e.
fun foo(callback: (String) -> Unit): Cancellable {
val task = CancellableImpl()
val job = GlobalScope.launch {
val fooResult = foo()
callback(fooResult)
}
task.cancellationCallback = {
job.cancel()
}
return task
}
Which drawbacks it has?
Whether GlobalScope is appropriate depends on your use case. If you use GlobalScope, it is up to the caller to manually cancel the task when it is no longer needed so it doesn’t hang onto resources and references longer than necessary.
Your current solution is not thread-safe and fires its callbacks on arbitrary threads from Dispatchers.Default. If you’re on Android, you may want to fire those callbacks on the Looper that the caller used to call your function.
Do you really need callbacks specifically? I would use the following so you don’t have to worry about implementing thread safety yourself.
GlobalScope.async { foo() }.asCompletableFuture()
Related
How to await for value using Coroutine suspend method in viewModel in most efficient way.
My solution for now:
viewModelScope.launch {
val item = async { itemRepository.getItem(id) }.await()
print("log me after not null $item)
}
Kotlin converts it to and the question is also why? :)
viewModelScope.launch {
withContext(Dispatchers.Default) {
itemRepository.getItem(id)
}
print("log me after not null $item)
}
my suspend funct
suspend fun geItem(itemId: Int): ItemRoom? = getItemByIdUseCase.build(itemId)
++ method from here \/
class GetItemByIdUseCase(private val itemDao: ItemDao) :
BaseUseCase<Int, ItemRoom>() {
override suspend fun create(id: Int): ItemRoom {
return itemDao.getItemById(id)
}
}
++ method from here \/
#Query("SELECT * FROM item WHERE id=:id")
abstract suspend fun getItemById(id: Int): ItemRoom
async { /* ... */ }.await() is a more convoluted version of withContext { }. There's no reason to use async if you are going to immediately await its result, because you are ultimately using it to synchronously get a result (synchronous, but suspending). The compiler probably warns you because if you do something that doesn't really make sense or is needlessly convoluted, it's likely you're doing something wrong or don't understand what you're doing.
In this case, you don't even need withContext(), because the only thing in your lambda is a suspend function call. You can freely call suspend functions synchronously inside coroutines and other suspend functions without worrying about coroutine context (as long as these functions are non-blocking, as any suspend function generated by Room would be).
viewModelScope.launch {
val item = itemRepository.getItem(id)
print("log me after not null $item")
}
This is the core concept of a suspend function. You can call it synchronously without it blocking the current thread.
I'm guessing the compiler warning just isn't sophisticated enough to tell that you don't need withContext, since it can't 100% guarantee you aren't calling a blocking function. Maybe it just assumes you were calling something blocking if you were trying to use async.
In my project I write View and ViewModel natively and share Repository, Db, networking.
When user navigates from one screen to another, I want to cancel all network requests or other heavy background operations that are currently running in the first screen.
Example function in Repository class:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
In Android's ViewModel I can use viewModelScope to automatically cancel all active coroutines. But how to cancel those tasks in iOS app?
Lets suppose that the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
I didn't find any first party information about this or any good solution, so I came up with my own. Shortly, it will require turning repository suspend functions to regular functions with return type of custom interface that has cancel() member function. Function will take action lambda as parameter. On implementation side, coroutine will be launched and reference for Job will be kept so later when it is required to stop background work interface cancel() function will cancel job.
In addition, because it is very hard to read type of error (in case it happens) from NSError, I wrapped return data with custom class which will hold error message and type. Earlier I asked related question but got no good answer for my case where ViewModel is written natively in each platform.
If you find any problems with this approach or have any ideas please share.
Custom return data wrapper:
class Result<T>(
val status: Status,
val value: T? = null,
val error: KError? = null
)
enum class Status {
SUCCESS, FAIL
}
data class KError(
val type: ErrorType,
val message: String? = null,
)
enum class ErrorType {
UNAUTHORIZED, CANCELED, OTHER
}
Custom interface
interface Cancelable {
fun cancel()
}
Repository interface:
//Convert this code inside of Repository interface:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
//To this:
fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable
Repository implementation:
override fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable = runInsideOfCancelableCoroutine {
val result = executeAndHandleExceptions {
val data = networkExample()
// do mapping, db operations, etc.
data
}
action.invoke(result)
}
// example of doing heavy background work
private suspend fun networkExample(): List<String> {
// delay, thread sleep
return listOf("data 1", "data 2", "data 3")
}
// generic function for reuse
private fun runInsideOfCancelableCoroutine(task: suspend () -> Unit): Cancelable {
val job = Job()
CoroutineScope(Dispatchers.Main + job).launch {
ensureActive()
task.invoke()
}
return object : Cancelable {
override fun cancel() {
job.cancel()
}
}
}
// generic function for reuse
private suspend fun <T> executeAndHandleExceptions(action: suspend () -> T?): Result<T> {
return try {
val data = action.invoke()
Result(status = Status.SUCCESS, value = data, error = null)
} catch (t: Throwable) {
Result(status = Status.FAIL, value = null, error = ErrorHandler.getError(t))
}
}
ErrorHandler:
object ErrorHandler {
fun getError(t: Throwable): KError {
when (t) {
is ClientRequestException -> {
try {
when (t.response.status.value) {
401 -> return KError(ErrorType.UNAUTHORIZED)
}
} catch (t: Throwable) {
}
}
is CancellationException -> {
return KError(ErrorType.CANCELED)
}
}
return KError(ErrorType.OTHER, t.stackTraceToString())
}
}
You probably have 3 options:
If you're using a some sort of reactive set up iOS side (e.g. MVVM) you could just choose to ignore cancellation. Cancellation will only save a minimal amount of work.
Wrap your iOS calls to shared code in an iOS reactive framework (e.g. combine) and handle cancellation using the iOS framework. The shared work would still be done, but the view won't be updated as your iOS framework is handling cancellation when leaving the screen.
Use Flow with this closable helper
I need to call a suspending function inside a suspendCoroutine block, before I call continuation.resume().
What is the appropriate way of doing that?
private suspend fun someFunction() = suspendCoroutine { cont ->
//...
val myResult = mySuspendingFunction() //<--- The IDE says "Suspension functions can be called only within coroutine body"
cont.resume(myResult)
}
You can't call a suspend function in suspendCoroutine block, because it accepts non suspend block as parameter:
suspend inline fun <T> suspendCoroutine(
crossinline block: (Continuation<T>) -> Unit
): T
'suspendCoroutine' mainly used when we have some legacy code with callbacks, e.g.:
suspend fun getUser(id: String): User = suspendCoroutine { continuation ->
Api.getUser(id) { user ->
continuation.resume(user)
}
}
If function someFunction() doesn't call Api with callbacks then you should reconsider your approach getting rid of 'suspendCoroutine':
private suspend fun someFunction() {
// ...
val myResult = mySuspendingFunction()
// ...
}
If you still want to use suspendCoroutine move call of mySuspendingFunction out of suspendCoroutine block:
private suspend fun someFunction(): String {
val myResult = mySuspendingFunction()
return suspendCoroutine { cont ->
//...
cont.resume(myResult)
}
}
suspend fun mySuspendingFunction(): String {
delay(1000) // simulate request
return "result"
}
It's best to avoid this and call the suspending function before suspendCoroutine, as others have answered. That is possible for the specific case in question.
However, that is not possible if you need the continuation.
(The following is for those, who found this question for the this reason, as #Zordid and I have. chan.send is an example of this.)
In which case, the following is a possible, but error prone way to do it, that I do not recommend:
suspend fun cont1() {
//btw. for correct implementation, this should most likely be at least suspendCancellableCoroutine
suspendCoroutine<Unit> { uCont ->
val x = suspend { chan.send(foo(uCont)) }
x.startCoroutine(Continuation(uCont.context) {
if (it.isFailure)
uCont.resumeWith(it)
// else resumed by whatever reads from chan
})
}
}
(I think the error handling alone illustrates why it's not a great option, despite other problems.)
A better, safer and cheaper way is to use CompletableDeferred if you can.
If you must pass in a Continuation, it's still safer and probably cheaper to do:
suspend fun cont2() {
val rslt = CompletableDeferred<Unit>()
chan.send(foo(Continuation(currentCoroutineContext()) {
rslt.completeWith(it)
}))
rslt.await()
}
How to return ArrayList from Coroutine?
GlobalScope.launch {
val list = retrieveData(firstDateOfMonth,lastDateOfMonth)
}
suspend fun retrieveData(
first: Date,
last: Date,
): ArrayList<Readings> = suspendCoroutine { c ->
var sensorReadingsList : ArrayList<Readings>?=null
GlobalScope.launch(Dispatchers.Main) {
val task2 = async {
WebApi.ReadingsList(
activity,auth_token, first, last
)
}
val resp2 = task2.await()
if (resp2?.status == "Success") {
sensorReadingsList = resp2?.organization_sensor_readings
}
c.resume(sensorReadingsList)
}
}
Error
Type inference failed: Cannot infer type parameter T in inline fun
Continuation.resume(value: T): Unit None of the following
substitutions receiver:
Continuation? /* =
java.util.ArrayList? */> arguments:
(kotlin.collections.ArrayList? /* =
java.util.ArrayList? */)
I guess WebApi.ReadingsList is a non-suspending function. That means that you'll need to make a thread wait while it runs. You probably don't want to use Dispatchers.Main for that, since that would run it on the UI thread. Dispatchers.IO would be the normal choice.
You also really shouldn't call suspendCoroutine for this. That's meant for low-level interoperation with other kinds of async callbacks, which you don't have in this case. Something like this would be more appropriate:
suspend fun retrieveData(
first: Date,
last: Date,
): ArrayList<Readings>? {
val resp2 = withContext(Dispatchers.IO) {
WebApi.ReadingsList(activity,auth_token, first, last)
}
if (resp2?.status == "Success") {
return resp2?.organization_sensor_readings
}
return null
}
This will run the blocking call in a subordinate job on an IO thread. This ensures that if your coroutine is cancelled, then the subordinate job will be cancelled too -- although that won't interrupt the blocking call.
I am creating a library and I'm using Retrofit with a call-adapter that gives me a Deferred<> value.
In a function in my code I call launch {}, and inside that i try-catch the values, and possible exceptions - calling different callbacks for different results.
The resources I've found on testing coroutines are all about testing suspended functions, and runBlocking {} is the solution to everything. Except for me it isn't
I made a quick example
#Mock
val mockListener: DoSomething.Listener = mock()
#Test
fun testSomething() {
val doer = DoSomething(mockListener)
runBlocking {
doer.doIt()
verify(mockListener).listen(any())
}
}
class DoSomething(val listener: Listener) {
interface Listener {
fun listen(s: String)
}
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
private fun theThing(): Deferred<String> {
return async {
delay(5, TimeUnit.SECONDS)
return#async "Wow, a thing"
}
}
}
What I want is for the actually run all functions. The test should take 5 seconds minimum, but it just runs through the code in a couple of millisconds- ie. it doesn't block.
I've tried adding
runBlocking {
launch {
// doer.doIt()
}.joinChildren()
}
And similar practices but I just can't get the test to actually wait for my launch inside of another class to finish before the test is finished.
Placing the verify(...) outside of the runBlocking also makes the test fail, which it should.
Any input, helpers, good practice etc. is appreciated!
You can provide the CoroutineContext explicitly for your doIt() function:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing().await()
}
}
With this parameter you could easily change the coroutine context - in your test code you use the blocking context:
runBlocking {
doer.doIt(coroutineContext)
}
BTW: You don't need to use launch and async. With launch you are in a suspendable context and you don't need to run theThing() asynchronously. Especially if you invoke await() in the next step:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing())
}
}
private suspend fun theThing(): String {
delay(5, TimeUnit.SECONDS)
return "Wow, a thing"
}
Best way would be not to swallow Job in your doIt() function as you do now.
Instead of
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
Do
fun doIt() = launch {
listener.listen(theThing().await())
}
That way your function will return a coroutine, which you can wait for:
doIt().join()
Better still is to use async() instead of launch()
Another comment is that doIt() should be actually doItAsync(), as suggested by Kotlin guidelines.