Skip to content

Commit 32178b8

Browse files
committed
Provide MainScope factory method
Fixes #829
1 parent 2272c37 commit 32178b8

File tree

3 files changed

+81
-12
lines changed

3 files changed

+81
-12
lines changed

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

+22
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,25 @@ public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
216216
@Suppress("FunctionName")
217217
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
218218
ContextScope(if (context[Job] != null) context else context + Job())
219+
220+
/**
221+
* Creates [CoroutineScope] for a UI components.
222+
* Resulting scope has [SupervisorJob] and [Dispatchers.Main] and inherits parent
223+
* and the rest of context elements from optionally provided [context].
224+
* If [context] already had a dispatcher, it will be overwritten by [Dispatchers.Main].
225+
*
226+
* Example of use:
227+
* ```
228+
* class MyAndroidActivity: CoroutineScope by MainScope(CoroutineName("MyActivity"))) {
229+
*
230+
* override fun onDestroy() {
231+
* super.onDestroy()
232+
* cancel() // CoroutineScope.cancel
233+
* }
234+
* }
235+
*
236+
* ```
237+
*/
238+
@Suppress("FunctionName")
239+
public fun MainScope(context: CoroutineContext = EmptyCoroutineContext): CoroutineScope =
240+
ContextScope(context + SupervisorJob(context[Job]) + Dispatchers.Main)

ui/coroutines-guide-ui.md

+5-11
Original file line numberDiff line numberDiff line change
@@ -472,26 +472,20 @@ 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() {
485+
492486
override fun onDestroy() {
493487
super.onDestroy()
494-
job.cancel()
488+
cancel() // CoroutineScope.cancel
495489
}
496490
}
497491
```

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

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

0 commit comments

Comments
 (0)