Consider an asynchronous API that reports progress on its operations:
suspend fun operationWithIO(input: String, progressUpdate: (String) -> Unit): String {
withContext(Dispatchers.IO) {
// ...
}
}
Is it possible to implement calls to progressUpdate such that callbacks are handled on the caller's dispatcher? Or is there a better way to deliver status updates back to the caller?
You should send progress updates on a channel. That will allow the caller to listen to the channel using whatever dispatcher it wants.
suspend fun operationWithIO(input: String, progressChannel: Channel<String>): String {
withContext(Dispatchers.IO) {
// ...
progressChannel.send("Done!")
progressChannel.close()
}
}
The caller can use it by doing something like this:
val progressChannel = Channel<String>()
someScope.launch {
operationWithIO(input, progressChannel)
}
// Remember the call to progressChannel.close(), so that this iteration stops.
for (progressUpdate in progressChannel) {
println(progressUpdate)
}
How about wrapping the callback function and calling the wrapped function:
/** Return a new callback that invokes this callback on the current context. */
suspend fun <T> ((T) -> Unit).onCurrentContext(): (T) -> Unit =
coroutineContext.let { context ->
{ value: T ->
runBlocking {
launch(context) {
this#onCurrentContext.invoke(value)
}
}
}
}
/** Perform a background operation, delivering status updates on the caller's context. */
suspend fun operationWithIO(statusUpdate: (String) -> Unit): String {
val cb = statusUpdate.onCurrentContext()
return withContext(Dispatchers.IO) {
cb("Phase 1")
delay(150)
cb("Phase 2")
delay(150)
"Result"
}
}
// In use
runBlocking {
val result = operationWithIO {
println("received callback status $it")
}
println("result is $result")
}
Related
I have a "high-order" function that have to return some value. Inside that "high-order" function there is an "inside" function which really is a producer of the return value of a "high-order" function.
It is simpler to show it with an example what I mean:
lifecycle.coroutineScope.launch {
val result = doWork()
Log.d("Tag", "some result: ${result.someString}")
}
private val service = SomeService()
suspend fun doWork(): DoWorkResult {
fun onSomeString(someString: String): DoWorkResult {
//some execution
val returnResultForDoWork = DoWorkResult(someString)
//How to return 'returnResultForDoWork' from fun doWork
return returnResultForDoWork
}
service.getString { someString ->
onSomeString(someString)
}
}
class SomeService() {
suspend fun getString(
onResult: (String) -> Unit
) {
delay(1000)
onResult("work is done")
}
}
data class DoWorkResult(val someString: String)
flow execution:
call service.getString
call onSomeString(someString) when a someString is return from service.getString
in onSomeString analyse/handle a someString and return (how?) a DoWorkResult(someString) from doWork
My question is how to return a result of an onSomeString function as a result of a doWork function?
Suspend functions don't need higher order callbacks like that. Really, it's an antipattern, because it restores back "callback hell" that coroutines solve. A proper version of your function would look like:
class SomeService() {
suspend fun getString(): String {
delay(1000)
return "work is done"
}
}
And then your calling function becomes:
suspend fun doWork(): DoWorkResult {
val serviceReturnValue = getString()
//some execution
val returnResultForDoWork = DoWorkResult(serviceReturnValue)
return returnResultForDoWork
}
But let's suppose your service function is not a suspend function, but rather it is asynchronous with a callback, and you don't have control over the source code to make it a suspend function instead.
class SomeService() {
fun getString(
onResult: (String) -> Unit
) {
val handler = Handler(Looper.myLooper())
thread {
Thread.sleep(1000) //simulate some work
handler.post { onResult("work is done") }
}
}
}
Then to be able to return the callback's inner value in a suspend function, you need to convert the asynchronous function into a suspending one. This can be done with suspendCoroutine or suspendCancellableCoroutine. There are many examples you can look up on this site or online, but here's a quick sample. You can write it as an extension function to work like an overloaded version of the asynchronous function.
suspend fun SomeService.getString(): String = suspendCoroutine { continuation ->
getString { continuation.resume(it) }
}
Now you can call this proper suspending version of the function just as in my second code block above.
Honestly, I am not quite sure if I really understand what you try to do but...
is this what you looking for?
private val service = SomeService()
data class DoWorkResult(val someString: String)
suspend fun doWork(): DoWorkResult {
fun onSomeString(someString: String): DoWorkResult {
//some execution
val returnResultForDoWork = DoWorkResult(someString)
//How to return 'returnResultForDoWork' from fun doWork
return returnResultForDoWork
}
return service.getString { someString ->
onSomeString(someString)
}
}
class SomeService {
suspend fun getString(onResult: (String) -> DoWorkResult): DoWorkResult {
delay(1000)
val myStringFromNetworkOrSomething = "work is done"
return onResult(myStringFromNetworkOrSomething)
}
}
The yield function in Kotlin states the following:
Yields a value to the Iterator being built and suspends until the next
value is requested.
In the following Kotlin code:
val sequence = sequence {
val start = 0
// yielding a single value
yield(start)
// yielding an iterable
yieldAll(1..5 step 2)
// yielding an infinite sequence
yieldAll(generateSequence(8) { it * 3 })
}
The yield statement is inside the sequence function. What I would like to do is have a Pause button that prevents further sequence items from being processed and a Resume button that when pressed allows the processing of the next item in the sequence. However if yield is inside of the sequence function, it isn't clear how a Pause/Resume flag can be used with this.
Sequences are just iterator suppliers, and iterators should be able to provide all elements without pauses:
for (elm in iterator) { }
// all elements must be created at this moment
// no additional elements allowed
Iterator can start sleeping during the next element providing. But, in this case, the caller thread (coroutine) will be blocked (suspended).
I assume you don't want to block threads, so I suggest creating a higher-order function that works like launch but returns Pauseable instead of Job:
fun CoroutineScope.launchPausable(block: suspend PauseableScope.() -> Unit): Pauseable =
PauseableCoroutine(this, block)
interface PauseableScope {
suspend fun pausePoint()
}
interface Pauseable {
fun pause()
fun resume()
val job: Job
}
class PauseableCoroutine(
scope: CoroutineScope,
block: suspend PauseableScope.() -> Unit
) : Pauseable, PauseableScope {
private var continuation: Continuation<Unit>? = null
private var isPaused = false
override val job = scope.launch { block() }
override fun pause() {
isPaused = true
}
override fun resume() {
syncIf({ isPaused }) {
isPaused = false
continuation?.resume(Unit)
continuation = null
}
}
override suspend fun pausePoint() {
suspendCoroutineUninterceptedOrReturn<Unit> { cont ->
syncIf({ isPaused }) {
continuation = cont
COROUTINE_SUSPENDED
} ?: Unit
}
}
}
Where syncIf is simple double-checked locking function:
inline fun <R> Any.syncIf(
crossinline condition: () -> Boolean,
crossinline block: () -> R
): R? {
if (condition())
synchronized(this) {
if (condition()) return block()
}
return null
}
Use case:
val pausable = GlobalScope.launchPausable {
items.forEach { item ->
doSomething(item)
// If it is paused coroutine will get suspended
pausePoint()
}
}
pausable.pause()
pausable.resume()
You can also put pausePoint() inside doSomething(item):
suspend fun PauseableScope.doSomething(item: Item) {
delay(100)
pausePoint()
delay(100)
}
I am writing a custom loop dsl and I want it's usage to look like below
var counter1 = 0
var counter2 = 0
loop {
counter1 += 1
println(counter1)
stopIf(counter1 == 5) // loop should terminate here and not execute rest of the code if condition matches
counter2 += 2
println(counter2)
stopIf(counter2 == 8) // loop should terminate here and not execute rest of the code if condition matches
}
I have following code which does allows me to write stopIf any number of times and anywhere in the loop body but when condition matches it does not terminate immediately but executes rest of the loop body and then terminates.
#UseExperimental(ExperimentalTime::class)
open class Loop {
var stop = false
val loopInterval = 1.seconds
suspend fun loop(block: suspend () -> Unit): Unit = loop(loopInterval, block)
suspend fun loop(minimumInterval: Duration, block: suspend () -> Unit): Unit =
loopWithoutDelay { delayedResult(maxOf(minimumInterval, loopInterval), block) }
private suspend fun loopWithoutDelay(block: suspend () -> Unit) {
block()
if (stop) return else loopWithoutDelay(block)
}
suspend fun <T> delayedResult(minDelay: Duration, f: suspend () -> T): T = coroutineScope {
val futureValue = async { f() }
delay(minDelay.toJavaDuration())
futureValue.await()
}
fun stopIf(condition: Boolean) {
if (condition) {
stop = condition // once stop condition matches, then do not override it with following false condtions
}
}
}
#ExperimentalTime
suspend fun loop(block: suspend Loop.() -> Unit) =
Loop().run { loop { block(this) } }
I have tried to use return with label but it did not work. Is there any way I can achieve this?
It can be done for example with throwing a lightweight exception. You have to declare custom exception:
class LoopStopException : Throwable("Stop look", null, false, false) // lightweight throwable without the stack trace
and catch it in loopWithoutDelay:
private suspend fun loopWithoutDelay(block: suspend () -> Unit) {
try {
while (true) {
block()
}
} catch (e: LoopStopException) {
//do nothing
}
}
I didn't understand much about the function delayedResult, because none of the dsl's public functions return a result. However, I come up with an solution for cancelling the loop.
As far as I understood, we have to have a loop that doesn't block the current thread. Therefore, it must be run in a coroutine, but in order to be able to cancel the loop, the dsl must run its own coroutine. This inner coroutine is run using coroutineScope, so it suspends the parent coroutine until it's finished or cancelled.
#ExperimentalTime
class Loop {
private val loopInterval = 1.seconds
suspend fun loop(block: suspend () -> Unit) = loop(loopInterval, block)
suspend fun loop(minimumInterval: Duration, block: suspend () -> Unit):Job = coroutineScope {
launch {
while (true) {
block()
delay(minOf(minimumInterval, loopInterval).toLongMilliseconds())
}
}
}
suspend fun stopIf(condition: Boolean) = coroutineScope {
suspendCancellableCoroutine<Unit> {
if (condition) it.cancel() else it.resumeWith(Result.success(Unit))
}
}
}
#ExperimentalTime
suspend fun loop(block: suspend Loop.() -> Unit):Job {
return Loop().run {
this.loop {
block(this)
}
}
}
I can't use "by lazy" because the callbacks require suspendCoroutine, which borks in android if it blocks the main thread, so I have to use the following "cache the result" pattern over and over. Is there a way to wrap it in a funButUseCachedResultsIfTheyAlreadyExist pattern to encapsulate the xCached object?
private var cameraDeviceCached: CameraDevice? = null
private suspend fun cameraDevice(): CameraDevice {
cameraDeviceCached?.also { return it }
return suspendCoroutine { cont: Continuation<CameraDevice> ->
... deep callbacks with cont.resume(camera) ...
}.also {
cameraDeviceCached = it
}
}
When what I'd really like to write is
private suspend fun cameraDevice(): CameraDevice = theMagicFunction { cont ->
... deep callbacks with cont.resume(camera) ...
}
You can build a generalized solution by wrapping an async call as follows:
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineStart.LAZY
class LazySuspendFun<out T>(
scope: CoroutineScope,
private val block: suspend () -> T
) {
private val deferred = scope.async(Dispatchers.Unconfined, LAZY) { block() }
suspend operator fun invoke() = deferred.await()
}
fun <T> CoroutineScope.lazySuspendFun(block: suspend () -> T) =
LazySuspendFun(this, block)
This is a simple example of how you can use it. Note that we are able to compose them so that we use a lazy-inited value as a dependency to getting another one:
val fetchToken = lazySuspendFun<String> {
suspendCoroutine { continuation ->
Thread {
info { "Fetching token" }
sleep(3000)
info { "Got token" }
continuation.resume("hodda_")
}.start()
}
}
val fetchPosts = lazySuspendFun<List<String>> {
val token = fetchToken()
suspendCoroutine { continuation ->
Thread {
info { "Fetching posts" }
sleep(3000)
info { "Got posts" }
continuation.resume(listOf("${token}post1", "${token}post2"))
}
}
}
On the calling side you must be inside some coroutine context so you can call the suspending functions:
myScope.launch {
val posts = fetchPosts()
...
}
This solution is robust enough that you can concurrently request the value several times and the initializer will run only once.
I'll write this as an answer, since it's not possible to post much code in comments.
What you're looking for is something like this:
private suspend fun cameraDevice() = theMagicFunction {
CameraDevice()
}()
suspend fun theMagicFunction(block: ()->CameraDevice): () -> CameraDevice {
var cameraDeviceCached: CameraDevice? = null
return fun(): CameraDevice {
cameraDeviceCached?.also { return it }
return suspendCoroutine { cont: Continuation<CameraDevice> ->
cont.resume(block())
}.also {
cameraDeviceCached = it
}
}
}
Unfortunately, this will not compile, since closures cannot be suspendable, and neither are local functions.
Best I can suggest, unless I miss a solution there, is to encapsulate this in a class, if this variable bothers you too much.
So I have a method that binds to the service.
fun bindService() {
val intent = Intent(this, BluetoothService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
Inside onCreate method I use this code:
bindService()
launch {
delay(500L)
service = serviceConnection.serviceBinder?.getService() as BluetoothService
}
Is there more elegant way to wait for the service to be bound than using delay()?
I wrote this just now, and haven't tried it, but hopefully something like it could work. The magic is in suspendCoroutine, which pauses the current coroutine and then gives you a continuation thingy you can use to resume it later. In our case we resume it when the onServiceConnected is called.
// helper class which holds data
class BoundService(
private val context: Context,
val name: ComponentName?,
val service: IBinder?,
val conn: ServiceConnection) {
fun unbind() {
context.unbindService(conn)
}
}
// call within a coroutine to bind service, waiting for onServiceConnected
// before the coroutine resumes
suspend fun bindServiceAndWait(context: Context, intent: Intent, flags: Int) = suspendCoroutine<BoundService> { continuation ->
val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
continuation.resume(BoundService(context, name, service, this))
}
override fun onServiceDisconnected(name: ComponentName?) {
// ignore, not much we can do
}
}
context.bindService(intent, conn, flags)
}
// just an example
suspend fun exampleUsage() {
val bs = bindServiceAndWait(context, intent, Context.BIND_AUTO_CREATE)
try {
// ...do something with bs.service...
} finally {
bs.unbind()
}
}