Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce MainScope factory and CoroutineScope.cancel extension #866

Merged
merged 3 commits into from
Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 the main [CoroutineScope] for UI components.
*
* Example of use:
* ```
* class MyAndroidActivity {
* private val scope = MainScope()
*
* override fun onDestroy() {
* super.onDestroy()
* scope.cancel()
* }
* }
*
* ```
*
* The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
* If you want to append additional elements to the 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 the scope does not have a job in it.
*
* This API is experimental in order to investigate possible clashes with other cancellation mechanisms.
*/
@Suppress("NOTHING_TO_INLINE")
@ExperimentalCoroutinesApi // Experimental and inline since 1.1.0, tentatively until 1.2.0
public inline fun CoroutineScope.cancel() {
val job = coroutineContext[Job] ?: error("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
18 changes: 6 additions & 12 deletions ui/coroutines-guide-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,28 +470,21 @@ collection of the whole trees of UI objects that were already destroyed and will

The natural solution to this problem is to associate a [Job] object with each UI object that has a lifecycle and create
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
it is easy to forget it. For this purpose, [CoroutineScope] interface could 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
54 changes: 54 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,55 @@ 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()
}
}