https://kotlinlang.org logo
Title
d

dewildte

05/04/2019, 6:34 PM
I am thinking about turning
View
clicks into
Flow<T>
, has anyone done this? What approach did you take? If you think it's a bad idea, why?
l

louiscad

05/04/2019, 6:38 PM
I made an
awaitOneClick(…)
suspending function and I prefer it that way, looping on it if needed. I actually put that into a library: https://github.com/LouisCAD/Splitties/tree/master/modules/views-coroutines
d

dewildte

05/04/2019, 6:43 PM
@louiscad Thanks for the reply, I was looking at your code and I am not sure how you react to the click.
I think your have a way better understanding of the continuation mechanism then I do.
l

louiscad

05/04/2019, 6:44 PM
I resume the continuation, which is essentially making the function return
So I handle the click on the line after the line where I call
awaitOneClick()
, simply.
d

dewildte

05/04/2019, 6:45 PM
Ok, That's what I thought. but was not sure.
That's very clever
And you said you loop inside the wrapping suspend function?
l

louiscad

05/04/2019, 6:47 PM
It's like waiting for network I/O, but here, it is UI I/O, where the output is enabling the view, and the input is the click.
d

dewildte

05/04/2019, 6:47 PM
Could you give me a small example of the implementation side?
l

louiscad

05/04/2019, 6:50 PM
Yes, I use a simple
while
loop where I wait for the click, then react to that, and once that's done, I let the loop interate again if I'm ready to accept further interactions. All the examples I have are closed source for the company I work for unfortunately, but I plan to use that in future sample code in open source projects. Let me write a dummy example…
d

dewildte

05/04/2019, 6:51 PM
Thank you so much!
My use case is going to be turning the clicks into a
Flow<T>
and then passing the flow to an EventBus for collection and redistribution.
l

louiscad

05/04/2019, 6:53 PM
Here's the simplest example I found on the top of my head:
while (true) {
    ui.launchDemoBtn.awaitOneClick()
    speakHello() // Suspends until done
}
d

dewildte

05/04/2019, 6:53 PM
Awesome!
you wrap the while loop into a
launch {...}
?
l

louiscad

05/04/2019, 6:54 PM
Note that I tend to hide this behind an
interface
for the user interface where I expose suspending functions and regular functions, with no dependencies on the platform though. You can call it coroutines enabled MV*
I have one launch per independent UI process. That makes it clear what can run concurrently and what runs sequentially.
d

dewildte

05/04/2019, 6:57 PM
Ok this is helpful, thanks!
l

louiscad

05/04/2019, 7:01 PM
When you use coroutines and UI together, it becomes increasingly important to have the root
CoroutineScope
cancelled when the view hierarchy is destroyed, that's why I made this to not write the boilerplate each time: https://github.com/LouisCAD/Splitties/tree/master/modules/lifecycle-coroutines If you're in a
Fragment
, be sure to use
viewLifecycleOwner
.
p

Paul Woitaschek

05/06/2019, 1:04 PM
Can't this be done through a channel?
fun View.clicks(): Flow<Unit> {
  return flowViaChannel { channel ->
    setOnClickListener {
      channel.offer(Unit)
    }
    channel.invokeOnClose {
      setOnClickListener(null)
    }
  }
}
d

Dominaezzz

05/06/2019, 1:08 PM
When do you close the channel?
p

Paul Woitaschek

05/06/2019, 1:26 PM
Shouldn't it be automatically get cancelled through the coroutine scope?
d

Dominaezzz

05/06/2019, 1:29 PM
What cancels the coroutine scope?
Nvm, I should just do some more reading on how Flow works. I'm not sure how it handles cancellation.
p

Paul Woitaschek

05/06/2019, 1:42 PM
Well I'm not sure either, would love to have some light here 🙂
@FlowPreview
fun main() {
  val flow = flowViaChannel<Unit> { channel ->
    coroutineScope {
      launch {
        repeat(10) {
          channel.offer(Unit)
        }
      }
      channel.invokeOnClose { println("closed") }
    }
  }.take(3)
  val result = runBlocking {
    flow.toList()
  }
  println("flow emitted $result")
}
This prints: closed flow emitted [kotlin.Unit, kotlin.Unit, kotlin.Unit]
d

Dominaezzz

05/06/2019, 1:57 PM
And without the
take(3)
?
p

Paul Woitaschek

05/06/2019, 2:01 PM
Without it doesn't close - why would it?
d

Dominaezzz

05/06/2019, 2:03 PM
That was the point I was initially trying to make.
p

Paul Woitaschek

05/06/2019, 2:05 PM
I don't get the point
d

Dominaezzz

05/06/2019, 2:36 PM
Same
😄 1
d

dewildte

05/06/2019, 7:55 PM
IT is safe to use though
And a nice implementation.
So I expanded on this idea and made
@FlowPreview
interface ChannelFlow<T> : SendChannel<T>, Flow<T>

@UseExperimental(FlowPreview::class)
fun <A> channelFlow(
    bufferSize: Int = 16
): ChannelFlow<A> {
    lateinit var c: SendChannel<A>

    val f = flowViaChannel<A>(bufferSize = bufferSize) {
        c = it
    }
    return object : ChannelFlow<A>, SendChannel<A> by c, Flow<A> by f {}
}
Implementation looks something like
@FlowPreview
class ViewFacadeImpl : ViewFacade {

    private val channelFlow = channelFlow<ViewEvent>()

    override val events: Flow<ViewEvent> = channelFlow

    override suspend fun render(viewState: ViewState) {
        channelFlow.send(object : ViewEvent {})
    }
}
hmm this seems to crash
The instantiation order is not as expected.
I had to rewrite to:
@UseExperimental(FlowPreview::class)
fun <A> channelFlow(
    bufferSize: Int = 16
): ChannelFlow<A> {

    val eventChannel: Channel<A> = Channel(bufferSize)

    val eventFlow: Flow<A> = flow {
        for (event in eventChannel) {
            emit(event)
        }
    }
    return object : ChannelFlow<A>,
        SendChannel<A> by eventChannel,
        Flow<A> by eventFlow {}
}