Skip to content

Commit 6802f7b

Browse files
Flow firstOrNull support (#1869)
* Flow firstOrNull support Co-authored-by: Bradyn Poulsen <[email protected]>
1 parent 1eaa309 commit 6802f7b

File tree

9 files changed

+128
-26
lines changed

9 files changed

+128
-26
lines changed

kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

+2
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,8 @@ public final class kotlinx/coroutines/flow/FlowKt {
907907
public static final fun filterNotNull (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
908908
public static final fun first (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
909909
public static final fun first (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
910+
public static final fun firstOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
911+
public static final fun firstOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
910912
public static final fun flatMap (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
911913
public static final fun flatMapConcat (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
912914
public static final fun flatMapLatest (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;

kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt

+36-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ public suspend fun <T: Any> Flow<T>.singleOrNull(): T? {
7777
if (result != null) error("Expected only one element")
7878
result = value
7979
}
80-
8180
return result
8281
}
8382

@@ -120,3 +119,39 @@ public suspend fun <T> Flow<T>.first(predicate: suspend (T) -> Boolean): T {
120119
if (result === NULL) throw NoSuchElementException("Expected at least one element matching the predicate $predicate")
121120
return result as T
122121
}
122+
123+
/**
124+
* The terminal operator that returns the first element emitted by the flow and then cancels flow's collection.
125+
* Returns `null` if the flow was empty.
126+
*/
127+
public suspend fun <T : Any> Flow<T>.firstOrNull(): T? {
128+
var result: T? = null
129+
try {
130+
collect { value ->
131+
result = value
132+
throw AbortFlowException(NopCollector)
133+
}
134+
} catch (e: AbortFlowException) {
135+
// Do nothing
136+
}
137+
return result
138+
}
139+
140+
/**
141+
* The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection.
142+
* Returns `null` if the flow did not contain an element matching the [predicate].
143+
*/
144+
public suspend fun <T : Any> Flow<T>.firstOrNull(predicate: suspend (T) -> Boolean): T? {
145+
var result: T? = null
146+
try {
147+
collect { value ->
148+
if (predicate(value)) {
149+
result = value
150+
throw AbortFlowException(NopCollector)
151+
}
152+
}
153+
} catch (e: AbortFlowException) {
154+
// Do nothing
155+
}
156+
return result
157+
}

kotlinx-coroutines-core/common/test/AsyncTest.kt

-6
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,6 @@ class AsyncTest : TestBase() {
210210
finish(13)
211211
}
212212

213-
class BadClass {
214-
override fun equals(other: Any?): Boolean = error("equals")
215-
override fun hashCode(): Int = error("hashCode")
216-
override fun toString(): String = error("toString")
217-
}
218-
219213
@Test
220214
fun testDeferBadClass() = runTest {
221215
val bad = BadClass()

kotlinx-coroutines-core/common/test/TestBase.common.kt

+5
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,8 @@ public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
8080

8181
public suspend fun wrapperDispatcher(): CoroutineContext = wrapperDispatcher(coroutineContext)
8282

83+
class BadClass {
84+
override fun equals(other: Any?): Boolean = error("equals")
85+
override fun hashCode(): Int = error("hashCode")
86+
override fun toString(): String = error("toString")
87+
}

kotlinx-coroutines-core/common/test/WithTimeoutOrNullTest.kt

-6
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,6 @@ class WithTimeoutOrNullTest : TestBase() {
152152
assertSame(bad, result)
153153
}
154154

155-
class BadClass {
156-
override fun equals(other: Any?): Boolean = error("Should not be called")
157-
override fun hashCode(): Int = error("Should not be called")
158-
override fun toString(): String = error("Should not be called")
159-
}
160-
161155
@Test
162156
fun testNullOnTimeout() = runTest {
163157
expect(1)

kotlinx-coroutines-core/common/test/WithTimeoutTest.kt

-6
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,6 @@ class WithTimeoutTest : TestBase() {
107107
assertSame(bad, result)
108108
}
109109

110-
class BadClass {
111-
override fun equals(other: Any?): Boolean = error("Should not be called")
112-
override fun hashCode(): Int = error("Should not be called")
113-
override fun toString(): String = error("Should not be called")
114-
}
115-
116110
@Test
117111
fun testExceptionOnTimeout() = runTest {
118112
expect(1)

kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt

-6
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,6 @@ class RendezvousChannelTest : TestBase() {
241241
finish(12)
242242
}
243243

244-
class BadClass {
245-
override fun equals(other: Any?): Boolean = error("equals")
246-
override fun hashCode(): Int = error("hashCode")
247-
override fun toString(): String = error("toString")
248-
}
249-
250244
@Test
251245
fun testProduceBadClass() = runTest {
252246
val bad = BadClass()

kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt

+77
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,81 @@ class FirstTest : TestBase() {
8383
assertEquals(1, flow.first())
8484
finish(2)
8585
}
86+
87+
@Test
88+
fun testFirstOrNull() = runTest {
89+
val flow = flowOf(1, 2, 3)
90+
assertEquals(1, flow.firstOrNull())
91+
}
92+
93+
@Test
94+
fun testFirstOrNullWithPredicate() = runTest {
95+
val flow = flowOf(1, 2, 3)
96+
assertEquals(1, flow.firstOrNull { it > 0 })
97+
assertEquals(2, flow.firstOrNull { it > 1 })
98+
assertNull(flow.firstOrNull { it > 3 })
99+
}
100+
101+
@Test
102+
fun testFirstOrNullCancellation() = runTest {
103+
val latch = Channel<Unit>()
104+
val flow = flow {
105+
coroutineScope {
106+
launch {
107+
latch.send(Unit)
108+
hang { expect(1) }
109+
}
110+
emit(1)
111+
emit(2)
112+
}
113+
}
114+
115+
116+
val result = flow.firstOrNull {
117+
latch.receive()
118+
true
119+
}
120+
assertEquals(1, result)
121+
finish(2)
122+
}
123+
124+
@Test
125+
fun testFirstOrNullWithEmptyFlow() = runTest {
126+
assertNull(emptyFlow<Int>().firstOrNull())
127+
assertNull(emptyFlow<Int>().firstOrNull { true })
128+
}
129+
130+
@Test
131+
fun testFirstOrNullWhenErrorCancelsUpstream() = runTest {
132+
val latch = Channel<Unit>()
133+
val flow = flow {
134+
coroutineScope {
135+
launch {
136+
latch.send(Unit)
137+
hang { expect(1) }
138+
}
139+
emit(1)
140+
}
141+
}
142+
143+
assertFailsWith<TestException> {
144+
flow.firstOrNull {
145+
latch.receive()
146+
throw TestException()
147+
}
148+
}
149+
150+
assertEquals(1, flow.firstOrNull())
151+
finish(2)
152+
}
153+
154+
@Test
155+
fun testBadClass() = runTest {
156+
val instance = BadClass()
157+
val flow = flowOf(instance)
158+
assertSame(instance, flow.first())
159+
assertSame(instance, flow.firstOrNull())
160+
assertSame(instance, flow.first { true })
161+
assertSame(instance, flow.firstOrNull { true })
162+
}
86163
}

kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ class SingleTest : TestBase() {
1717

1818
assertEquals(239L, flow.single())
1919
assertEquals(239L, flow.singleOrNull())
20-
2120
}
2221

2322
@Test
@@ -63,4 +62,12 @@ class SingleTest : TestBase() {
6362
assertNull(flowOf<Int?>(null).single())
6463
assertFailsWith<NoSuchElementException> { flowOf<Int?>().single() }
6564
}
65+
66+
@Test
67+
fun testBadClass() = runTest {
68+
val instance = BadClass()
69+
val flow = flowOf(instance)
70+
assertSame(instance, flow.single())
71+
assertSame(instance, flow.singleOrNull())
72+
}
6673
}

0 commit comments

Comments
 (0)