Skip to content

Commit

Permalink
Introduce MainScope factory and CoroutineScope.cancel extension
Browse files Browse the repository at this point in the history
Fixes #829
  • Loading branch information
qwwdfsad committed Dec 10, 2018
1 parent 3c4168b commit e64b52c
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 12 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ GlobalScope.launch {

## Modules

* [common](common/README.md) — common coroutines across all backends:
* [common](common/README.md) — common coroutines across all platforms:
* `launch` and `async` coroutine builders;
* `Job` and `Deferred` light-weight future with cancellation support;
* `MainScope` for Android and UI applications.
* `Dispatchers` object with `Main` dispatcher for Android/Swing/JavaFx, and `Default` dispatcher for background coroutines;
* `delay` and `yield` top-level suspending functions;
* `Channel` and `Mutex` communication and synchronization primitives;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ public abstract interface class kotlinx/coroutines/CoroutineScope {

public final class kotlinx/coroutines/CoroutineScopeKt {
public static final fun CoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope;
public static final fun MainScope ()Lkotlinx/coroutines/CoroutineScope;
public static final fun cancel (Lkotlinx/coroutines/CoroutineScope;)V
public static final fun coroutineScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun isActive (Lkotlinx/coroutines/CoroutineScope;)Z
public static final fun plus (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope;
Expand Down
36 changes: 36 additions & 0 deletions common/kotlinx-coroutines-core-common/src/CoroutineScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ public interface CoroutineScope {
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
ContextScope(coroutineContext + context)

/**
* Creates [CoroutineScope] for a UI components.
*
* Example of use:
* ```
* class MyAndroidActivity {
* private val scope = MainScope()
*
* override fun onDestroy() {
* super.onDestroy()
* scope.cancel()
* }
* }
*
* ```
*
* Resulting scope has [SupervisorJob] and [Dispatchers.Main].
* If you want to append additional elements to main scope, use [CoroutineScope.plus] operator:
* `val scope = MainScope() + CoroutineName("MyActivity") `.
*/
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

/**
* Returns `true` when current [Job] is still active (has not completed and was not cancelled yet).
*
Expand Down Expand Up @@ -172,3 +195,16 @@ public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())

/**
* Cancels this scope, including its job and all its children.
* Throws [IllegalStateException] if scope does not have a job in it.
*
* This API is experimental in order to investigate possible clashes with other cancellation mechanism.
*/
@Suppress("NOTHING_TO_INLINE")
@ExperimentalCoroutinesApi // Experimental and inline until 1.2
public inline fun CoroutineScope.cancel() {
val job = coroutineContext[Job] ?: error("Current scope cannot be cancelled because it does not have a job: $this")
job.cancel()
}
1 change: 1 addition & 0 deletions core/kotlinx-coroutines-core/src/Dispatchers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public actual object Dispatchers {

/**
* A coroutine dispatcher that is confined to the Main thread operating with UI objects.
* This dispatcher can be used either directly or via [MainScope] factory.
* Usually such dispatcher is single-threaded.
*
* Access to this property may throw [IllegalStateException] if no main thread dispatchers are present in the classpath.
Expand Down
16 changes: 5 additions & 11 deletions ui/coroutines-guide-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,26 +472,19 @@ The natural solution to this problem is to associate a [Job] object with each UI
all the coroutines in the context of this job. But passing associated job object to every coroutine builder is error-prone,
it is easy to forget it. For this purpose, [CoroutineScope] interface should be implemented by UI owner, and then every
coroutine builder defined as an extension on [CoroutineScope] inherits UI job without explicitly mentioning it.
For the sake of simplicity, [MainScope()] factory can be used. It automatically provides `Dispatchers.Main` and parent
job.

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

```kotlin
abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope {
protected lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}

abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() {
override fun onDestroy() {
super.onDestroy()
job.cancel()
cancel() // CoroutineScope.cancel
}
}
```
Expand Down Expand Up @@ -711,6 +704,7 @@ After delay
[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html
[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
[MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
Expand Down
52 changes: 52 additions & 0 deletions ui/kotlinx-coroutines-swing/test/SwingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ package kotlinx.coroutines.swing

import kotlinx.coroutines.*
import org.junit.*
import org.junit.Test
import javax.swing.*
import kotlin.coroutines.*
import kotlin.test.*

class SwingTest : TestBase() {
@Before
Expand All @@ -29,4 +32,53 @@ class SwingTest : TestBase() {
job.join()
finish(6)
}

private class SwingComponent(coroutineContext: CoroutineContext = EmptyCoroutineContext) :
CoroutineScope by MainScope() + coroutineContext {
public var executed = false
fun testLaunch(): Job = launch {
check(SwingUtilities.isEventDispatchThread())
executed = true
}
fun testFailure(): Job = launch {
check(SwingUtilities.isEventDispatchThread())
throw TestException()
}
fun testCancellation() : Job = launch(start = CoroutineStart.ATOMIC) {
check(SwingUtilities.isEventDispatchThread())
delay(Long.MAX_VALUE)
}
}
@Test
fun testLaunchInMainScope() = runTest {
val component = SwingComponent()
val job = component.testLaunch()
job.join()
assertTrue(component.executed)
component.cancel()
component.coroutineContext[Job]!!.join()
}

@Test
fun testFailureInMainScope() = runTest {
var exception: Throwable? = null
val component = SwingComponent(CoroutineExceptionHandler { ctx, e -> exception = e})
val job = component.testFailure()
job.join()
assertTrue(exception!! is TestException)
component.cancel()
join(component)
}

@Test
fun testCancellationInMainScope() = runTest {
val component = SwingComponent()
component.cancel()
component.testCancellation().join()
join(component)
}

private suspend fun join(component: SwingTest.SwingComponent) {
component.coroutineContext[Job]!!.join()
}
}

0 comments on commit e64b52c

Please sign in to comment.