1

I am trying to write a class to easily chain code run in different coroutine contexts.

Ideally, I would like to use it like this:

        io {
            // Run IO code that returns an object (nullable)
        } ui { ioResult->
            // Run UI code using the returned object (non-nullable)
        } ifNull {
            // Run UI code when the returned object is null
        }

What I have so far works like this:

        GlobalScope.launch {
            CoroutineLinker(null).io {
                // Run IO code
            } ui { ioResult ->
                ioResult?.also {
                    // Run UI code after null check
                } ?: run {
                    // Run UI code when null
                }
            } ifNull {
                // Redundant block
            }
        }

As you can see there is still quite some work left but I am stuck, so I share this with you:

    class CoroutineLinker<T> (
        private val value: T?
    ) {

        suspend infix fun <K> io (block: suspend () -> K?): CoroutineLinker<K?> {
            return withContext(Dispatchers.IO) {
                CoroutineLinker(block())
            }
        }

        suspend infix fun ui (block: suspend (value: T) -> Unit): CoroutineLinker<T> {
            return withContext(Dispatchers.Main) {

                if (value != null ) {
                    block(value)
                }

                this@CoroutineLinker
            }
        }

        suspend infix fun ifNull (block: suspend () -> Unit) {
            return withContext(Dispatchers.Main) {
                if (value == null) {
                    block()
                }
            }
        }
    }

Any input is welcome! :)

JonZarate
  • 841
  • 6
  • 28
  • Starting with your concept and ignoring the implementation for now: how do you know that `ifNull` in your example references the result of `io` and not the result of `ui`? Do you consider your tool only usable in IO -> UI cases, so IO is always a producer and UI is always a consumer? – broot Jul 15 '21 at 19:09
  • I want this to read an API and update the UI through `ViewModels`, however, outside Android `ViewModels` can't be updated from another thread than the Main/UI. So yes, `io` is always going to be the producer and `ui` the consumer. – JonZarate Jul 15 '21 at 19:19
  • Unfortunatelly, your recommendation doesn't work, I get a `Type mismatch` in the `io` method. – JonZarate Jul 15 '21 at 19:20
  • Your first comment made me think I might need another class so the results can be different. – JonZarate Jul 15 '21 at 19:21
  • 1
    I added my answer. Also, this is only an opinion, but I think you should really reconsider if this is at all needed. I know, we sometimes like to make some weird voodoo magic to save 10 chars of code here and there, but after some time we see that it was really overengineering. Your case looks exactly like this. – broot Jul 15 '21 at 19:30

1 Answers1

1

I think this will do what you need:

suspend fun <K : Any> io (block: suspend () -> K?) = CoroutineLinker(null).io(block)

class CoroutineLinker<T : Any> (
    private val value: T?
) {

    suspend infix fun <K : Any> io (block: suspend () -> K?): CoroutineLinker<K> {
        return withContext(Dispatchers.IO) {
            CoroutineLinker(block())
        }
    }

    suspend infix fun ui (block: suspend (value: T) -> Unit): CoroutineLinker<T> {
        if (value != null ) {
            withContext(Dispatchers.Main) {
                block(value)
            }
        }

        return this
    }

    suspend infix fun ifNull (block: suspend () -> Unit) {
        if (value == null) {
            withContext(Dispatchers.Main) {
                block()
            }
        }
    }
}

I changed 3 things:

  • Added upper bounds for CoroutineLinker to Any.
  • Added io function.
  • Changed the order of if and withContext in both functions - this is just optimization, it wasn't required.
broot
  • 21,588
  • 3
  • 30
  • 35
  • Also, if `io` is always at the beginning of the chain then I guess it makes sense to remove `CoroutineLinker.io()` function and move its contents to the external `io()`. – broot Jul 15 '21 at 19:37
  • You are probably right, this looks like overengineering :) Thanks! – JonZarate Jul 15 '21 at 19:59