With the release of version 0.26.0 of coroutines, a new, important feature was introduced—coroutine scope. All of the coroutine builders from the coroutines-core library are extension functions of the CoroutineScope interface.
The CoroutineScope interface looks as follows:
public interface CoroutineScope {
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
public val isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
public val coroutineContext: CoroutineContext
}
We need the coroutine scope to provide a proper cancellation mechanism for the coroutines that we launch in our application. Modern frameworks, such as Android SDK or React Native, are built in such a way that all components, and the application itself, have a life cycle. In Android SDK, this can be an activity or a fragment, and in React Native, it can be a component.
The coroutine scope represents a scope of an object that has a life cycle, such as an activity or a component. The coroutines-core library provides a scope for an entire application, and we can use it if we want to launch a coroutine that works as long as an application runs. The scope of the entire application is represented by the GlobalScope object, and looks as follows:
object GlobalScope : CoroutineScope {
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
override val isActive: Boolean
get() = true
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
Let's create a new activity with its own coroutine scope. The easiest way to do this is to call the context menu of a package and choose the New section, which looks as follows:
Then, choose the Empty Activity option in the Activity subsection, as follows:
Android Studio will open the Configure Activity window, where you can change a configuration of Activity and press the Finish button:
A newly created XKCDActivity class will look as follows:
class XKCDActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_xkcd)
}
}
If we want to launch a life cycle aware coroutine from this class, we should implement the CoroutineScope interface, as follows:
class XKCDActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_xkcd)
}
}
The CoroutineScope interface looks as follows:
public interface CoroutineScope {
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
public val isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
public val coroutineContext: CoroutineContext
}
The XKCDActivity class implements the CoroutineScope interface and overrides the coroutineContext property. The overridden coroutineContext property contains a getter that returns Dispatchers.Main.
The Dispatchers is an object from the coroutines-core library, which contains the following dispatchers:
- Default is used by all standard coroutine builders, such as launch or async
- Main is used to run a coroutine on the main thread
- Unconfident invokes a coroutine immediately, on the first available thread
- IO is used to run coroutines that perform input/output operations
Since a getter of the overridden coroutineContext property returns the Main dispatcher, all coroutine builders from this class will launch coroutines that work on the main thread.
The XKCDActivity has its own coroutine scope, but it is not life cycle aware. This means that a coroutine launched in a scope of this activity will not be destroyed if the activity is destroyed. We can fix this in the following way:
class XKCDActivity : AppCompatActivity(), CoroutineScope {
private lateinit var lifecycleAwareJob: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + lifecycleAwareJob
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_xkcd)
lifecycleAwareJob = Job()
}
override fun onDestroy() {
super.onDestroy()
lifecycleAwareJob.cancel()
}
}
The lifecycleAwareJob will be used as a parent for all coroutines, and will cancel all child coroutines when an activity is destroyed. The following example code shows how to use this approach:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_xkcd)
lifecycleAwareJob = Job()
launch {
val image = async(Dispatchers.IO) { loadImage() }.await()
showImage(image)
}
}
The launch coroutine builder creates a coroutine that works on the main thread, and the async coroutine builder creates a coroutine that works on the input/output thread. When the image is ready, it will be shown on the main thread of the application. If we press the back button, the coroutines will be destroyed, along with XKCDActivity.