Skip to content

Commit

Permalink
Update CoroutineScope docs (#1882)
Browse files Browse the repository at this point in the history
* Update CoroutineScope docs
* Fixed scope examples in guides, added notes on first-party support in Android.
* Simplified scopes section in UI guide since it is mostly irrelevant.

Fixes #1581
  • Loading branch information
elizarov authored and qwwdfsad committed Apr 24, 2020
1 parent 08dcd10 commit f12ef3c
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 102 deletions.
34 changes: 11 additions & 23 deletions docs/coroutine-context-and-dispatchers.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,21 +501,8 @@ class Activity {

</div>

Alternatively, we can implement the [CoroutineScope] interface in this `Activity` class. The best way to do it is
to use delegation with default factory functions.
We also can combine the desired dispatcher (we used [Dispatchers.Default] in this example) with the scope:

<div class="sample" markdown="1" theme="idea" data-highlight-only>

```kotlin
class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
// to be continued ...
```

</div>

Now, we can launch coroutines in the scope of this `Activity` without having to explicitly
specify their context. For the demo, we launch ten coroutines that delay for a different time:
Now, we can launch coroutines in the scope of this `Activity` using the defined `scope`.
For the demo, we launch ten coroutines that delay for a different time:

<div class="sample" markdown="1" theme="idea" data-highlight-only>

Expand All @@ -524,7 +511,7 @@ specify their context. For the demo, we launch ten coroutines that delay for a d
fun doSomething() {
// launch ten coroutines for a demo, each working for a different time
repeat(10) { i ->
launch {
mainScope.launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
Expand All @@ -544,21 +531,19 @@ of the activity no more messages are printed, even if we wait a little longer.
<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">

```kotlin
import kotlin.coroutines.*
import kotlinx.coroutines.*

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {

class Activity {
private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes

fun destroy() {
cancel() // Extension on CoroutineScope
mainScope.cancel()
}
// to be continued ...

// class Activity continues
fun doSomething() {
// launch ten coroutines for a demo, each working for a different time
repeat(10) { i ->
launch {
mainScope.launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
Expand Down Expand Up @@ -597,6 +582,9 @@ Destroying activity!
As you can see, only the first two coroutines print a message and the others are cancelled
by a single invocation of `job.cancel()` in `Activity.destroy()`.

> Note, that Android has first-party support for coroutine scope in all entities with the lifecycle.
See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope).

### Thread-local data

Sometimes it is convenient to have an ability to pass some thread-local data to or between coroutines.
Expand Down
38 changes: 25 additions & 13 deletions kotlinx-coroutines-core/common/src/CoroutineScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,49 @@ import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*

/**
* Defines a scope for new coroutines. Every coroutine builder
* Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc)
* is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext]
* to automatically propagate both context elements and cancellation.
* to automatically propagate all its elements and cancellation.
*
* The best ways to obtain a standalone instance of the scope are [CoroutineScope()] and [MainScope()] factory functions.
* Additional context elements can be appended to the scope using the [plus][CoroutineScope.plus] operator.
*
* ### Convention for structured concurrency
*
* Manual implementation of this interface is not recommended, implementation by delegation should be preferred instead.
* By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a [job][Job] to enforce structured concurrency.
* By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a
* [job][Job] to enforce the discipline of **structured concurrency** with propagation of cancellation.
*
* Every coroutine builder (like [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc)
* Every coroutine builder (like [launch], [async], etc)
* and every scoping function (like [coroutineScope], [withContext], etc) provides _its own_ scope
* with its own [Job] instance into the inner block of code it runs.
* By convention, they all wait for all the coroutines inside their block to complete before completing themselves,
* thus enforcing the discipline of **structured concurrency**.
* thus enforcing the structured concurrency. See [Job] documentation for more details.
*
* [CoroutineScope] should be implemented (or used as a field) on entities with a well-defined lifecycle that are responsible
* for launching children coroutines. Example of such entity on Android is Activity.
* Usage of this interface may look like this:
* ### Android usage
*
* Android has first-party support for coroutine scope in all entities with the lifecycle.
* See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope).
*
* ### Custom usage
*
* [CoroutineScope] should be implemented or declared as a property on entities with a well-defined lifecycle that are
* responsible for launching children coroutines, for example:
*
* ```
* class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
* override fun onDestroy() {
* cancel() // cancel is extension on CoroutineScope
* class MyUIClass {
* val scope = MainScope() // the scope of MyUIClass
*
* fun destroy() { // destroys an instance of MyUIClass
* scope.cancel() // cancels all coroutines launched in this scope
* // ... do the rest of cleanup here ...
* }
*
* /*
* * Note how coroutine builders are scoped: if activity is destroyed or any of the launched coroutines
* * Note: if this instance is destroyed or any of the launched coroutines
* * in this method throws an exception, then all nested coroutines are cancelled.
* */
* fun showSomeData() = launch { // <- extension on current activity, launched in the main thread
* fun showSomeData() = scope.launch { // launched in the main thread
* // ... here we can use suspending functions or coroutine builders with other dispatchers
* draw(data) // draw in the main thread
* }
Expand Down
12 changes: 5 additions & 7 deletions kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@
// This file was automatically generated from coroutine-context-and-dispatchers.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.exampleContext10

import kotlin.coroutines.*
import kotlinx.coroutines.*

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {

class Activity {
private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes

fun destroy() {
cancel() // Extension on CoroutineScope
mainScope.cancel()
}
// to be continued ...

// class Activity continues
fun doSomething() {
// launch ten coroutines for a demo, each working for a different time
repeat(10) { i ->
launch {
mainScope.launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
Expand Down
81 changes: 22 additions & 59 deletions ui/coroutines-guide-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,47 +408,40 @@ during UI freeze.
### Structured concurrency, lifecycle and coroutine parent-child hierarchy

A typical UI application has a number of elements with a lifecycle. Windows, UI controls, activities, views, fragments
and other visual elements are created and destroyed. A long-running coroutine, performing some IO or a background
computation, can retain references to the corresponding UI elements for longer than it is needed, preventing garbage
and other visual elements are created and destroyed. A long-running coroutine, performing some IO or a background
computation, can retain references to the corresponding UI elements for longer than it is needed, preventing garbage
collection of the whole trees of UI objects that were already destroyed and will not be displayed anymore.

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 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.
The natural solution to this problem is to associate a [CoroutineScope] object with each UI object that has a
lifecycle and create all the coroutines in the context of this scope.
For the sake of simplicity, [MainScope()] factory can be used. It automatically provides `Dispatchers.Main` and
a parent job for all the children coroutines.

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 `CoroutineScope` to an instance of an `Activity`:

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 by MainScope() {
class MainActivity : AppCompatActivity() {
private val scope = MainScope()

override fun onDestroy() {
super.onDestroy()
cancel() // CoroutineScope.cancel
scope.cancel()
}
}
```

Now, an activity that is associated with a job has to extend ScopedAppActivity

```kotlin
class MainActivity : ScopedAppActivity() {

fun asyncShowData() = launch { // Is invoked in UI context with Activity's job as a parent
fun asyncShowData() = scope.launch { // Is invoked in UI context with Activity's scope as a parent
// actual implementation
}

suspend fun showIOData() {
val deferred = async(Dispatchers.IO) {
// impl
val data = withContext(Dispatchers.IO) {
// compute data in background thread
}
withContext(Dispatchers.Main) {
val data = deferred.await()
// Show data in UI
// Show data in UI
}
}
}
Expand All @@ -457,43 +450,14 @@ class MainActivity : ScopedAppActivity() {
Every coroutine launched from within a `MainActivity` has its job as a parent and is immediately cancelled when
activity is destroyed.

To propagate activity scope to its views and presenters, multiple techniques can be used:
- [coroutineScope] builder to provide a nested scope
- Receive [CoroutineScope] in presenter method parameters
- Make method extension on [CoroutineScope] (applicable only for top-level methods)

```kotlin
class ActivityWithPresenters: ScopedAppActivity() {
fun init() {
val presenter = Presenter()
val presenter2 = ScopedPresenter(this)
}
}

class Presenter {
suspend fun loadData() = coroutineScope {
// Nested scope of outer activity
}

suspend fun loadData(uiScope: CoroutineScope) = uiScope.launch {
// Invoked in the uiScope
}
}

class ScopedPresenter(scope: CoroutineScope): CoroutineScope by scope {
fun loadData() = launch { // Extension on ActivityWithPresenters's scope
}
}

suspend fun CoroutineScope.launchInIO() = launch(Dispatchers.IO) {
// Launched in the scope of the caller, but with IO dispatcher
}
```
> Note, that Android has first-party support for coroutine scope in all entities with the lifecycle.
See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope).

Parent-child relation between jobs forms a hierarchy. A coroutine that performs some background job on behalf of
the view and in its context can create further children coroutines. The whole tree of coroutines gets cancelled
the activity can create further children coroutines. The whole tree of coroutines gets cancelled
when the parent job is cancelled. An example of that is shown in the
["Children of a coroutine"](../docs/coroutine-context-and-dispatchers.md#children-of-a-coroutine) section of the guide to coroutines.

<!--- CLEAR -->

### Blocking operations
Expand Down Expand Up @@ -649,7 +613,6 @@ After delay
[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
[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
Expand Down

0 comments on commit f12ef3c

Please sign in to comment.