From e64b52cfb1e9ae21dfd9ebf3d4f20dc3db62aef2 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 30 Nov 2018 14:47:53 +0300 Subject: [PATCH] Introduce MainScope factory and CoroutineScope.cancel extension Fixes #829 --- README.md | 3 +- .../kotlinx-coroutines-core.txt | 2 + .../src/CoroutineScope.kt | 36 +++++++++++++ .../src/Dispatchers.kt | 1 + ui/coroutines-guide-ui.md | 16 ++---- ui/kotlinx-coroutines-swing/test/SwingTest.kt | 52 +++++++++++++++++++ 6 files changed, 98 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e2e9364d07..1ecfd8fb60 100644 --- a/README.md +++ b/README.md @@ -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; diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index bbb4b1d998..32d7d058af 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -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; diff --git a/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt b/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt index 1ab48b02c9..3f31c52258 100644 --- a/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt +++ b/common/kotlinx-coroutines-core-common/src/CoroutineScope.kt @@ -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). * @@ -172,3 +195,16 @@ public suspend fun 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() +} diff --git a/core/kotlinx-coroutines-core/src/Dispatchers.kt b/core/kotlinx-coroutines-core/src/Dispatchers.kt index ca3d4c02c5..454f5991a1 100644 --- a/core/kotlinx-coroutines-core/src/Dispatchers.kt +++ b/core/kotlinx-coroutines-core/src/Dispatchers.kt @@ -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. diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 349da739e8..e04410f7a1 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -472,6 +472,8 @@ 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 @@ -479,19 +481,10 @@ instance of a `Job` to an instance of an `Activity`: ```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 } } ``` @@ -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 diff --git a/ui/kotlinx-coroutines-swing/test/SwingTest.kt b/ui/kotlinx-coroutines-swing/test/SwingTest.kt index cc6e286a72..5b16d5fb96 100644 --- a/ui/kotlinx-coroutines-swing/test/SwingTest.kt +++ b/ui/kotlinx-coroutines-swing/test/SwingTest.kt @@ -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 @@ -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() + } } \ No newline at end of file