https://kotlinlang.org logo
Title
s

Saiedmomen

08/15/2019, 5:47 PM
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

louiscad

08/15/2019, 6:42 PM
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

Saiedmomen

08/15/2019, 6:47 PM
Is it collect's general behavior o is it because of awaitClose? Also why androids lifecycleScope isn't affected?
l

louiscad

08/15/2019, 6:50 PM
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

Saiedmomen

08/15/2019, 7:05 PM
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

louiscad

08/15/2019, 7:25 PM
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

Saiedmomen

08/15/2019, 7:35 PM
yeah that works. thank you!
l

louiscad

08/15/2019, 7:41 PM
BTW, if you want to support backpressure by disabling the button until the click is processed, you can use this snippet:
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

Saiedmomen

08/15/2019, 8:02 PM
that's super helpful thank you!