Skip to content

Commit 4ea4078

Browse files
authored
Merge pull request #2337 from Kotlin/version-1.4.0
Version 1.4.0
2 parents 768e92b + 00da1ac commit 4ea4078

File tree

80 files changed

+1655
-2026
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+1655
-2026
lines changed

CHANGES.md

+24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Change log for kotlinx.coroutines
22

3+
## Version 1.4.0
4+
5+
### Improvements
6+
7+
* `StateFlow`, `SharedFlow` and corresponding operators are promoted to stable API (#2316).
8+
* `Flow.debounce` operator with timeout selector based on each individual element is added (#1216, thanks to @mkano9!).
9+
* `CoroutineContext.job` extension property is introduced (#2159).
10+
* `Flow.combine operator` is reworked:
11+
* Complete fairness is maintained for single-threaded dispatchers.
12+
* Its performance is improved, depending on the use-case, by at least 50% (#2296).
13+
* Quadratic complexity depending on the number of upstream flows is eliminated (#2296).
14+
* `crossinline` and `inline`-heavy internals are removed, fixing sporadic SIGSEGV on Mediatek Android devices (#1683, #1743).
15+
* `Flow.zip` operator performance is improved by 40%.
16+
* Various API has been promoted to stable or its deprecation level has been raised (#2316).
17+
18+
### Bug fixes
19+
20+
* Suspendable `stateIn` operator propagates exception to the caller when upstream fails to produce initial value (#2329).
21+
* Fix `SharedFlow` with replay for subscribers working at different speed (#2325).
22+
* Do not fail debug agent installation when security manager does not provide access to system properties (#2311).
23+
* Cancelled lazy coroutines are properly cleaned up from debug agent output (#2294).
24+
* `BlockHound` false-positives are correctly filtered out (#2302, #2190, #2303).
25+
* Potential crash during a race between cancellation and upstream in `Observable.asFlow` is fixed (#2104, #2299, thanks to @LouisCAD and @drinkthestars).
26+
327
## Version 1.4.0-M1
428

529
### Breaking changes

README.md

+11-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
44
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)
5-
[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0-M1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0-M1)
5+
[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0)
66
[![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org)
77
[![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/)
88

@@ -29,7 +29,7 @@ suspend fun main() = coroutineScope {
2929
* [delay] and [yield] top-level suspending functions;
3030
* [Flow] — cold asynchronous stream with [flow][_flow] builder and comprehensive operator set ([filter], [map], etc);
3131
* [Channel], [Mutex], and [Semaphore] communication and synchronization primitives;
32-
* [coroutineScope], [supervisorScope], [withContext], and [withTimeout] scope builders;
32+
* [coroutineScope][_coroutineScope], [supervisorScope][_supervisorScope], [withContext], and [withTimeout] scope builders;
3333
* [MainScope()] for Android and UI applications;
3434
* [SupervisorJob()] and [CoroutineExceptionHandler] for supervision of coroutines hierarchies;
3535
* [select] expression support and more.
@@ -86,7 +86,7 @@ Add dependencies (you can also add other modules that you need):
8686
<dependency>
8787
<groupId>org.jetbrains.kotlinx</groupId>
8888
<artifactId>kotlinx-coroutines-core</artifactId>
89-
<version>1.4.0-M1</version>
89+
<version>1.4.0</version>
9090
</dependency>
9191
```
9292

@@ -104,7 +104,7 @@ Add dependencies (you can also add other modules that you need):
104104

105105
```groovy
106106
dependencies {
107-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1'
107+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
108108
}
109109
```
110110

@@ -130,7 +130,7 @@ Add dependencies (you can also add other modules that you need):
130130

131131
```groovy
132132
dependencies {
133-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1")
133+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0")
134134
}
135135
```
136136

@@ -152,7 +152,7 @@ In common code that should get compiled for different platforms, you can add dep
152152
```groovy
153153
commonMain {
154154
dependencies {
155-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1")
155+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0")
156156
}
157157
}
158158
```
@@ -163,7 +163,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android)
163163
module as dependency when using `kotlinx.coroutines` on Android:
164164

165165
```groovy
166-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1'
166+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
167167
```
168168

169169
This gives you access to Android [Dispatchers.Main]
@@ -190,15 +190,15 @@ packagingOptions {
190190
### JS
191191

192192
[Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as
193-
[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0-M1/jar)
193+
[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0/jar)
194194
(follow the link to get the dependency declaration snippet).
195195

196196
You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM.
197197

198198
### Native
199199

200200
[Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as
201-
[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0-M1/jar)
201+
[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0/jar)
202202
(follow the link to get the dependency declaration snippet).
203203

204204
Only single-threaded code (JS-style) on Kotlin/Native is currently supported.
@@ -227,8 +227,8 @@ See [Contributing Guidelines](CONTRIBUTING.md).
227227
[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
228228
[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
229229
[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
230-
[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
231-
[supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
230+
[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
231+
[_supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
232232
[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
233233
[withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html
234234
[MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html

benchmarks/build.gradle.kts

+2-28
Original file line numberDiff line numberDiff line change
@@ -31,38 +31,12 @@ tasks.named<KotlinCompile>("compileJmhKotlin") {
3131
}
3232
}
3333

34-
/*
35-
* Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths,
36-
* and it breaks JMH that tries to post-process these symbols and fails because they are renamed.
37-
*/
38-
val removeRedundantFiles by tasks.registering(Delete::class) {
39-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class")
40-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class")
41-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class")
42-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$1.class")
43-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$2\$1.class")
44-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$2\$1.class")
45-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$2\$1.class")
46-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$1\$1.class")
47-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$2\$1.class")
48-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class")
49-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class")
50-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class")
51-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/SaneFlowPlaysScrabble\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class")
5234

53-
// Primes
54-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$2\$1.class")
55-
delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$1.class")
56-
}
57-
58-
tasks.named("jmhRunBytecodeGenerator") {
59-
dependsOn(removeRedundantFiles)
60-
}
6135

6236
// It is better to use the following to run benchmarks, otherwise you may get unexpected errors:
6337
// ./gradlew --no-daemon cleanJmhJar jmh -Pjmh="MyBenchmark"
6438
extensions.configure<JMHPluginExtension>("jmh") {
65-
jmhVersion = "1.21"
39+
jmhVersion = "1.26"
6640
duplicateClassesStrategy = DuplicatesStrategy.INCLUDE
6741
failOnError = true
6842
resultFormat = "CSV"
@@ -80,7 +54,7 @@ tasks.named<Jar>("jmhJar") {
8054
}
8155

8256
dependencies {
83-
compile("org.openjdk.jmh:jmh-core:1.21")
57+
compile("org.openjdk.jmh:jmh-core:1.26")
8458
compile("io.projectreactor:reactor-core:${version("reactor")}")
8559
compile("io.reactivex.rxjava2:rxjava:2.1.9")
8660
compile("com.github.akarnokd:rxjava2-extensions:0.20.8")

benchmarks/src/jmh/kotlin/benchmarks/ChannelSinkBenchmark.kt

+18
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,22 @@ open class ChannelSinkBenchmark {
5050
for (i in start until (start + count))
5151
send(i)
5252
}
53+
54+
// Migrated from deprecated operators, are good only for stressing channels
55+
56+
private fun <E> ReceiveChannel<E>.filter(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel<E> =
57+
GlobalScope.produce(context, onCompletion = { cancel() }) {
58+
for (e in this@filter) {
59+
if (predicate(e)) send(e)
60+
}
61+
}
62+
63+
private suspend inline fun <E, R> ReceiveChannel<E>.fold(initial: R, operation: (acc: R, E) -> R): R {
64+
var accumulator = initial
65+
consumeEach {
66+
accumulator = operation(accumulator, it)
67+
}
68+
return accumulator
69+
}
5370
}
71+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package benchmarks.flow
6+
7+
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.flow.*
9+
import org.openjdk.jmh.annotations.*
10+
import java.util.concurrent.*
11+
12+
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
13+
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
14+
@Fork(value = 1)
15+
@BenchmarkMode(Mode.Throughput)
16+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
17+
@State(Scope.Benchmark)
18+
open class CombineFlowsBenchmark {
19+
20+
@Param("10", "100", "1000")
21+
private var size = 10
22+
23+
@Benchmark
24+
fun combine() = runBlocking {
25+
combine((1 until size).map { flowOf(it) }) { a -> a}.collect()
26+
}
27+
28+
@Benchmark
29+
fun combineTransform() = runBlocking {
30+
val list = (1 until size).map { flowOf(it) }.toList()
31+
combineTransform((1 until size).map { flowOf(it) }) { emit(it) }.collect()
32+
}
33+
}
34+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package benchmarks.flow
6+
7+
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.flow.*
9+
import kotlinx.coroutines.flow.internal.*
10+
import org.openjdk.jmh.annotations.*
11+
import java.util.concurrent.*
12+
13+
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
14+
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
15+
@Fork(value = 1)
16+
@BenchmarkMode(Mode.Throughput)
17+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
18+
@State(Scope.Benchmark)
19+
open class CombineTwoFlowsBenchmark {
20+
21+
@Param("100", "100000", "1000000")
22+
private var size = 100000
23+
24+
@Benchmark
25+
fun combinePlain() = runBlocking {
26+
val flow = (1 until size.toLong()).asFlow()
27+
flow.combine(flow) { a, b -> a + b }.collect()
28+
}
29+
30+
@Benchmark
31+
fun combineTransform() = runBlocking {
32+
val flow = (1 until size.toLong()).asFlow()
33+
flow.combineTransform(flow) { a, b -> emit(a + b) }.collect()
34+
}
35+
36+
@Benchmark
37+
fun combineVararg() = runBlocking {
38+
val flow = (1 until size.toLong()).asFlow()
39+
combine(listOf(flow, flow)) { arr -> arr[0] + arr[1] }.collect()
40+
}
41+
42+
@Benchmark
43+
fun combineTransformVararg() = runBlocking {
44+
val flow = (1 until size.toLong()).asFlow()
45+
combineTransform(listOf(flow, flow)) { arr -> emit(arr[0] + arr[1]) }.collect()
46+
}
47+
}

benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,14 @@ open class NumbersBenchmark {
7777

7878
@Benchmark
7979
fun zipRx() {
80-
val numbers = rxNumbers().take(natural.toLong())
80+
val numbers = rxNumbers().take(natural)
8181
val first = numbers
8282
.filter { it % 2L != 0L }
8383
.map { it * it }
8484
val second = numbers
8585
.filter { it % 2L == 0L }
8686
.map { it * it }
87-
first.zipWith(second, BiFunction<Long, Long, Long> { v1, v2 -> v1 + v2 }).filter { it % 3 == 0L }.count()
87+
first.zipWith(second, { v1, v2 -> v1 + v2 }).filter { it % 3 == 0L }.count()
8888
.blockingGet()
8989
}
9090

@@ -98,7 +98,7 @@ open class NumbersBenchmark {
9898

9999
@Benchmark
100100
fun transformationsRx(): Long {
101-
return rxNumbers().take(natural.toLong())
101+
return rxNumbers().take(natural)
102102
.filter { it % 2L != 0L }
103103
.map { it * it }
104104
.filter { (it + 1) % 3 == 0L }.count()

buildSrc/build.gradle.kts

+13-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ plugins {
99
}
1010

1111
val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true
12+
val buildSnapshotTrain = properties["build_snapshot_train"]?.toString()?.toBoolean() == true
1213

1314
repositories {
1415
if (cacheRedirectorEnabled) {
@@ -20,6 +21,10 @@ repositories {
2021
maven("https://dl.bintray.com/kotlin/kotlin-eap")
2122
maven("https://dl.bintray.com/kotlin/kotlin-dev")
2223
}
24+
25+
if (buildSnapshotTrain) {
26+
mavenLocal()
27+
}
2328
}
2429

2530
kotlinDslPluginOptions {
@@ -30,8 +35,14 @@ val props = Properties().apply {
3035
file("../gradle.properties").inputStream().use { load(it) }
3136
}
3237

33-
fun version(target: String): String =
34-
props.getProperty("${target}_version")
38+
fun version(target: String): String {
39+
// Intercept reading from properties file
40+
if (target == "kotlin") {
41+
val snapshotVersion = properties["kotlin_snapshot_version"]
42+
if (snapshotVersion != null) return snapshotVersion.toString()
43+
}
44+
return props.getProperty("${target}_version")
45+
}
3546

3647
dependencies {
3748
implementation(kotlin("gradle-plugin", version("kotlin")))

buildSrc/settings.gradle.kts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
/*
22
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
4-
54
pluginManagement {
5+
val build_snapshot_train: String? by settings
66
repositories {
77
val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true
8-
98
if (cacheRedirectorEnabled) {
109
println("Redirecting repositories for buildSrc buildscript")
11-
1210
maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2")
1311
} else {
1412
maven("https://plugins.gradle.org/m2")
1513
}
14+
if (build_snapshot_train?.toBoolean() == true) {
15+
mavenLocal()
16+
}
1617
}
1718
}

docs/basics.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,12 @@ World!
235235
### Scope builder
236236

237237
In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using the
238-
[coroutineScope] builder. It creates a coroutine scope and does not complete until all launched children complete.
238+
[coroutineScope][_coroutineScope] builder. It creates a coroutine scope and does not complete until all launched children complete.
239239

240-
[runBlocking] and [coroutineScope] may look similar because they both wait for their body and all its children to complete.
240+
[runBlocking] and [coroutineScope][_coroutineScope] may look similar because they both wait for their body and all its children to complete.
241241
The main difference is that the [runBlocking] method _blocks_ the current thread for waiting,
242-
while [coroutineScope] just suspends, releasing the underlying thread for other usages.
243-
Because of that difference, [runBlocking] is a regular function and [coroutineScope] is a suspending function.
242+
while [coroutineScope][_coroutineScope] just suspends, releasing the underlying thread for other usages.
243+
Because of that difference, [runBlocking] is a regular function and [coroutineScope][_coroutineScope] is a suspending function.
244244

245245
It can be demonstrated by the following example:
246246

@@ -281,7 +281,7 @@ Coroutine scope is over
281281
-->
282282

283283
Note that right after the "Task from coroutine scope" message (while waiting for nested launch)
284-
"Task from runBlocking" is executed and printed — even though the [coroutineScope] is not completed yet.
284+
"Task from runBlocking" is executed and printed — even though the [coroutineScope][_coroutineScope] is not completed yet.
285285

286286
### Extract function refactoring
287287

@@ -403,7 +403,7 @@ Active coroutines that were launched in [GlobalScope] do not keep the process al
403403
[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
404404
[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
405405
[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
406-
[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
406+
[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
407407
[CoroutineScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html
408408
<!--- END -->
409409

0 commit comments

Comments
 (0)