Skip to content

Commit 41ff119

Browse files
committed
Introduce MainScope factory and CoroutineScope.cancel extension
Fixes #829
1 parent a5b6a33 commit 41ff119

File tree

6 files changed

+98
-12
lines changed

6 files changed

+98
-12
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ GlobalScope.launch {
1919

2020
## Modules
2121

22-
* [common](common/README.md) — common coroutines across all backends:
22+
* [common](common/README.md) — common coroutines across all platforms:
2323
* `launch` and `async` coroutine builders;
2424
* `Job` and `Deferred` light-weight future with cancellation support;
25+
* `MainScope` for Android and UI applications.
2526
* `Dispatchers` object with `Main` dispatcher for Android/Swing/JavaFx, and `Default` dispatcher for background coroutines;
2627
* `delay` and `yield` top-level suspending functions;
2728
* `Channel` and `Mutex` communication and synchronization primitives;

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

+2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ public abstract interface class kotlinx/coroutines/CoroutineScope {
168168

169169
public final class kotlinx/coroutines/CoroutineScopeKt {
170170
public static final fun CoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope;
171+
public static final fun MainScope ()Lkotlinx/coroutines/CoroutineScope;
172+
public static final fun cancel (Lkotlinx/coroutines/CoroutineScope;)V
171173
public static final fun coroutineScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
172174
public static final fun isActive (Lkotlinx/coroutines/CoroutineScope;)Z
173175
public static final fun plus (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope;

common/kotlinx-coroutines-core-common/src/CoroutineScope.kt

+36
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ public interface CoroutineScope {
7373
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
7474
ContextScope(coroutineContext + context)
7575

76+
/**
77+
* Creates [CoroutineScope] for a UI components.
78+
*
79+
* Example of use:
80+
* ```
81+
* class MyAndroidActivity {
82+
* private val scope = MainScope()
83+
*
84+
* override fun onDestroy() {
85+
* super.onDestroy()
86+
* scope.cancel()
87+
* }
88+
* }
89+
*
90+
* ```
91+
*
92+
* Resulting scope has [SupervisorJob] and [Dispatchers.Main].
93+
* If you want to append additional elements to main scope, use [CoroutineScope.plus] operator:
94+
* `val scope = MainScope() + CoroutineName("MyActivity") `.
95+
*/
96+
@Suppress("FunctionName")
97+
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
98+
7699
/**
77100
* Returns `true` when current [Job] is still active (has not completed and was not cancelled yet).
78101
*
@@ -172,3 +195,16 @@ public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
172195
@Suppress("FunctionName")
173196
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
174197
ContextScope(if (context[Job] != null) context else context + Job())
198+
199+
/**
200+
* Cancels this scope, including its job and all its children.
201+
* Throws [IllegalStateException] if scope does not have a job in it.
202+
*
203+
* This API is experimental in order to investigate possible clashes with other cancellation mechanism.
204+
*/
205+
@Suppress("NOTHING_TO_INLINE")
206+
@ExperimentalCoroutinesApi // Experimental and inline until 1.2
207+
public inline fun CoroutineScope.cancel() {
208+
val job = coroutineContext[Job] ?: error("Current scope cannot be cancelled because it does not have a job: $this")
209+
job.cancel()
210+
}

core/kotlinx-coroutines-core/src/Dispatchers.kt

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public actual object Dispatchers {
3333

3434
/**
3535
* A coroutine dispatcher that is confined to the Main thread operating with UI objects.
36+
* This dispatcher can be used either directly or via [MainScope] factory.
3637
* Usually such dispatcher is single-threaded.
3738
*
3839
* Access to this property may throw [IllegalStateException] if no main thread dispatchers are present in the classpath.

ui/coroutines-guide-ui.md

+5-11
Original file line numberDiff line numberDiff line change
@@ -472,26 +472,19 @@ The natural solution to this problem is to associate a [Job] object with each UI
472472
all the coroutines in the context of this job. But passing associated job object to every coroutine builder is error-prone,
473473
it is easy to forget it. For this purpose, [CoroutineScope] interface should be implemented by UI owner, and then every
474474
coroutine builder defined as an extension on [CoroutineScope] inherits UI job without explicitly mentioning it.
475+
For the sake of simplicity, [MainScope()] factory can be used. It automatically provides `Dispatchers.Main` and parent
476+
job.
475477

476478
For example, in Android application an `Activity` is initially _created_ and is _destroyed_ when it is no longer
477479
needed and when its memory must be released. A natural solution is to attach an
478480
instance of a `Job` to an instance of an `Activity`:
479481
<!--- CLEAR -->
480482

481483
```kotlin
482-
abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope {
483-
protected lateinit var job: Job
484-
override val coroutineContext: CoroutineContext
485-
get() = job + Dispatchers.Main
486-
487-
override fun onCreate(savedInstanceState: Bundle?) {
488-
super.onCreate(savedInstanceState)
489-
job = Job()
490-
}
491-
484+
abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() {
492485
override fun onDestroy() {
493486
super.onDestroy()
494-
job.cancel()
487+
cancel() // CoroutineScope.cancel
495488
}
496489
}
497490
```
@@ -711,6 +704,7 @@ After delay
711704
[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
712705
[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html
713706
[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
707+
[MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
714708
[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
715709
[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
716710
[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html

ui/kotlinx-coroutines-swing/test/SwingTest.kt

+52
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ package kotlinx.coroutines.swing
66

77
import kotlinx.coroutines.*
88
import org.junit.*
9+
import org.junit.Test
910
import javax.swing.*
11+
import kotlin.coroutines.*
12+
import kotlin.test.*
1013

1114
class SwingTest : TestBase() {
1215
@Before
@@ -29,4 +32,53 @@ class SwingTest : TestBase() {
2932
job.join()
3033
finish(6)
3134
}
35+
36+
private class SwingComponent(coroutineContext: CoroutineContext = EmptyCoroutineContext) :
37+
CoroutineScope by MainScope() + coroutineContext {
38+
public var executed = false
39+
fun testLaunch(): Job = launch {
40+
check(SwingUtilities.isEventDispatchThread())
41+
executed = true
42+
}
43+
fun testFailure(): Job = launch {
44+
check(SwingUtilities.isEventDispatchThread())
45+
throw TestException()
46+
}
47+
fun testCancellation() : Job = launch(start = CoroutineStart.ATOMIC) {
48+
check(SwingUtilities.isEventDispatchThread())
49+
delay(Long.MAX_VALUE)
50+
}
51+
}
52+
@Test
53+
fun testLaunchInMainScope() = runTest {
54+
val component = SwingComponent()
55+
val job = component.testLaunch()
56+
job.join()
57+
assertTrue(component.executed)
58+
component.cancel()
59+
component.coroutineContext[Job]!!.join()
60+
}
61+
62+
@Test
63+
fun testFailureInMainScope() = runTest {
64+
var exception: Throwable? = null
65+
val component = SwingComponent(CoroutineExceptionHandler { ctx, e -> exception = e})
66+
val job = component.testFailure()
67+
job.join()
68+
assertTrue(exception!! is TestException)
69+
component.cancel()
70+
join(component)
71+
}
72+
73+
@Test
74+
fun testCancellationInMainScope() = runTest {
75+
val component = SwingComponent()
76+
component.cancel()
77+
component.testCancellation().join()
78+
join(component)
79+
}
80+
81+
private suspend fun join(component: SwingTest.SwingComponent) {
82+
component.coroutineContext[Job]!!.join()
83+
}
3284
}

0 commit comments

Comments
 (0)