I'm starting to explore flows and I've decided to ...
# coroutines
s
I'm starting to explore flows and I've decided to try and deliver android view clicks with it. Here what I've come up with. It works great in android but the
runBlocking
block in test gets stuck at
collect
call. I think the
awaitClose
call suspends the
runBlocking
scope but doesn't affect the android code somehow. Can you please tell me if I'm doing it right and how I can write unit tests for it?
l
This is expected. Collect suspends until the flow is completed, which here is never the case. You probably want to use
take(5)
or launch 2 coroutines in parallel.
s
Is it collect's general behavior o is it because of awaitClose? Also why androids lifecycleScope isn't affected?
l
It's because your
Flow
is infinite. It can always have new elements caused by new clicks, unless you stop collecting by using another operator or by throwing or cancelling.
lifecycleScope
isn't much related. It's an auto-cancelling root scope to launch your lifecycle bound coroutines in.
s
the thing is that it never gets to having elements. Code on line 38 wasn't getting called
Thats probably why adding take didn't have any effect
I also tried enclosing the collect in a launch. Now the test runs to completion and offer(L4) is called 5 times as expected but the code in FlowCollectors emit isn't getting called and clicksCounter isn't incremented(L34).
l
Use
collect
extension instead of
FlowCollector
.
👍 1
Also, make sure you don't wait for collection to complete while you call
performClick()
. Then,
cancelAndJoin()
on the collection coroutine, and perform the assertion.
👍 1
s
yeah that works. thank you!
l
BTW, if you want to support backpressure by disabling the button until the click is processed, you can use this snippet:
Copy code
fun View.clicksFlow(
    disableAfterClick: Boolean = true,
    hideAfterClick: Boolean = false
): Flow<Unit> = flow {
    while (true) emit(
        awaitOneClick(
            disableAfterClick = disableAfterClick,
            hideAfterClick = hideAfterClick
        )
    )
}

suspend fun View.awaitOneClick(
    disableAfterClick: Boolean = true,
    hideAfterClick: Boolean = false
) = try {
    if (disableAfterClick) isEnabled = true
    if (hideAfterClick) isVisible = true
    suspendCancellableCoroutine<Unit> { continuation ->
        setOnClickListener {
            setOnClickListener(null) // Ensure we can't get two clicks in a row.
            continuation.resume(Unit)
        }
    }
} finally {
    setOnClickListener(null)
    if (disableAfterClick) isEnabled = false
    if (hideAfterClick) isVisible = false
}
👍 2
s
that's super helpful thank you!