diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index e379842443..d0ce20945e 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -501,21 +501,8 @@ class Activity { -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: - -
- -```kotlin - class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) { - // to be continued ... -``` - -
- -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:
@@ -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") } @@ -544,21 +531,19 @@ of the activity no more messages are printed, even if we wait a little longer.
```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") } @@ -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. diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index a6b79bdb5a..7b5c645d1f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -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 * } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt b/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt index 0256004405..73ecbd2b3e 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt @@ -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") } diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 20607be21a..e6cc2081e1 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -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`: ```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 } } } @@ -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. + ### Blocking operations @@ -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