Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”
This article also participated in the “Digitalstar Project” to win a creative gift package and creative incentive money
đ If you are a beginner to Coroutines on the Android platform, check out the previous article: The Android Coroutines Series: Getting Started
đ CoroutineScope
What is a CoroutineScope? If that sounds unfamiliar to you, GlobalScope, lifecycleScope, and viewModelScope are (for Android developers, of course). They all implement the CoroutineScope interface.
public interface CoroutineScope {
/** * The context of this scope. * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope. * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages. * * By convention, should contain an instance of a [job][Job] to enforce structured concurrency. */
public val coroutineContext: CoroutineContext
}
Copy the code
The CoroutineScope contains only one variable to be implemented CoroutineContext(introduced in the previous article)
By its structure, we can think of it as a container that provides CoroutineContext, ensures that CoroutineContext can be passed through the entire coroutine operation, and constrains the boundary of CoroutineContext.
For example, if you use a coroutine to request data in Android, the Activity will exit before the interface request completes, and not stopping the running coroutine can have unexpected consequences. So we recommend using lifecycleScope to start coroutines in all activities. LifecycleScope gives coroutines the same lifecycle awareness as activities.
đŠ ī¸ GlobalScope
/** * A global [CoroutineScope] not bound to any job. * * Global scope is used to launch top-level coroutines which are operating on the whole application lifetime * and are not cancelled prematurely. * Another use of the global scope is operators running in [Dispatchers.Unconfined], which don't have any job associated with them. * * Application code usually should use an application-defined [CoroutineScope]. Using * [async][CoroutineScope.async] or [launch][CoroutineScope.launch] * on the instance of [GlobalScope] is highly discouraged. * * Usage of this interface may look like this: * * ``` * fun ReceiveChannel
.sqrt(): ReceiveChannel
= GlobalScope.produce(Dispatchers.Unconfined) { * for (number in this) { * send(Math.sqrt(number)) * } * } * ``` */
public object GlobalScope : CoroutineScope {
/** * Returns [EmptyCoroutineContext]. */
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
Copy the code
- Global (
It's actually a singleton
) The CoroutineScope has no restrictions. - Global scope is used to start top-level coroutines that run throughout the application life cycle and are not cancelled prematurely. Another use of global scopes is operators running in dispatchers.unconfined, which have no associated work.
- Application code should normally use the CoroutineScope defined by the application. Asynchrony or startup is not recommended on a GlobalScope instance.
đŠ ī¸ lifecycleScope
Using lifecycleScope requires introducing dependencies
implementation 'androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0'//lifecycleScope
Copy the code
LifecycleScope source code:
LifecycleOwner extension functions
/** * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle]. * * This scope will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
Copy the code
Extension functions for Lifecycle
/** * [CoroutineScope] tied to this [Lifecycle]. * * This scope will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] */
val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get(a)as LifecycleCoroutineScopeImpl?
if(existing ! =null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
Copy the code
It creates a LifecycleCoroutineScopeImpl instance, it implements the CoroutineScope interface, at the same time the incoming SupervisorJob () + Dispatchers. The Main CoroutineContext as it.
Take a look at the newscope.register () method
internal class LifecycleCoroutineScopeImpl(
override val lifecycle: Lifecycle,
override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
init {
// in case we are initialized on a non-main thread, make a best effort check before
// we return the scope. This is not sync but if developer is launching on a non-main
// dispatcher, they cannot be 100% sure anyways.
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
coroutineContext.cancel()
}
}
fun register(a) {
launch(Dispatchers.Main.immediate) {
if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)}else {
coroutineContext.cancel()
}
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
lifecycle.removeObserver(this)
coroutineContext.cancel()
}
}
}
Copy the code
A coroutine is created using the classic launch in the register method, and the CoroutineContext used by launch is the CoroutineContext in CoroutineSope. The Lifecycle feature of Jetpack is then combined with the coroutine to listen for the Lifecycle of the Activiyt.
If you are not familiar with the use and features of Lifecycle, please read the Jetpack family Bucket (2) for Lifecycle use and source code.
When an Activity is destroyed, CoroutineContext is used. CoroutineContext itself has no cancel method. This Cancel method is an extension of CoroutineContext.
/** * Cancels [Job] of this context with an optional cancellation cause. * See [Job.cancel] for details. */
public fun CoroutineContext.cancel(cause: CancellationException? = null) {
this[Job]? .cancel(cause) }Copy the code
So the real logic from CoroutineContex set to retrieve the Key Job for instance, the corresponding is created above LifecycleCoroutineScopeImpl instances when the incoming SupervisorJob, It is one of the subclasses of CoroutineContext.
đŠī¸ How can I implement the same functionality of lifecycleScope myself?
class MainActivity : AppCompatActivity() {
/** 1. Create a MainScope ** (CoroutineScope can also be implemented) * class MainActivity: AppCompatActivity(), CoroutineScope{ * lateinit var job: Job * override val coroutineContext: CoroutineContext * get() = Dispatchers.Main + job * override fun onCreate(savedInstanceState: Bundle?) { * super.onCreate(savedInstanceState) * job = Job() * } * override fun onDestroy() { * super.onDestroy() * job.cancel() // Cancel job on activity destroy. After destroy all children jobs will be cancelled automatically * } * } */
private val scope = CoroutineScope(Dispatchers.Main + Job())
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadData()
}
private fun loadData(a) {
//2. At the start of the IO thread (dispatchers. IO switch to the IO thread)
scope.launch(Dispatchers.IO) {
//3. The IO thread pulls data
val result = fetchData()
//4. The main thread updates data
withContext(Dispatchers.Main) {
/ / 5. Update the UI
findViewById<TextView>(R.id.tvShowContent).text = result
}
}
}
// Key word suspend
private suspend fun fetchData(a): String {
delay(5000)
return "Data content"
}
override fun onDestroy(a) {
super.onDestroy()
// Coroutine cancelled
scope.cancel()
}
}
Copy the code
It also ensures that the coroutine terminates when the Activity is destroyed.
đŠ ī¸ viewModelScope (ditto)
implementation 'androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0'//viewModelScope
Copy the code
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if(scope ! =null) {
return scope
}
// Dispatchers.Main is used by default to facilitate activities and fragments updating the UI
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
// In order for the ViewModel to be able to remove coroutines, the Closeable interface needs to be implemented
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close(a) {
coroutineContext.cancel()
}
}
Copy the code
abstract class ViewModel
// ViewModel stores CoroutineScope objects via HashMap
private final Map<String, Object> mBagOfTags = new HashMap<>();
// Clear () is executed internally when the ViewModel is destroyed,
@MainThread
final void clear() {
mCleared = true;
if(mBagOfTags ! =null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// Remove the coroutine from viewModelScope scope
closeWithRuntimeException(value);
}
}
}
onCleared();
}
/ / cancel CoroutineScope
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
thrownew RuntimeException(e); }}}// Return object if JOB_KEY already exists and the corresponding coroutine scope is not empty, otherwise create a new coroutine scope and set JOB_KEY
@SuppressWarnings("unchecked")
<T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);
if (previous == null) {
mBagOfTags.put(key, newValue);
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {
closeWithRuntimeException(result);
}
return result;
}
// Return the corresponding coroutine scope from the HashMap via JOB_KEY
@SuppressWarnings({"TypeParameterUnusedInFormals"."unchecked"})
<T> T getTag(String key) {
synchronized (mBagOfTags) {
return (T) mBagOfTags.get(key); }}}Copy the code
conclusion
- The GlobalScope lifecycle is process-level, and the coroutine is still executing even after the Activity or Fragment has been destroyed.
- LifecycleScope can only be used in activities and fragments and is bound to their lifecycles
- ViewModelScope can only be used within a ViewModel, bound to the ViewModel lifecycle