Key words: Kotlin coroutine Android Anko
Using coroutines on Android instead of callbacks or RxJava is actually quite easy, and we can even control the execution state of coroutines on a larger scale with the lifecycle of the UI
This article involves MainScope and AutoDispose source code: Kotlin-Coroutines-Android
1. Configure dependencies
We mentioned that if we’re developing on Android, we need to introduce
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutine_version'
Copy the code
The framework includes a Dispatcher for Android, which can be accessed via dispatchers.main. MainScope is also included for integration with the Android scope.
Anko also provides some handy methods, such as onClick, to introduce its dependencies if needed:
// Provides a convenient listener similar to onClick to receive suspend Lambda expressions
implementation "org.jetbrains.anko:anko-sdk27-coroutines:$anko_version"
// We provide BG and asReference. We have not yet followed up kotlin 1.3's official coroutine, but the code is relatively simple, and we can modify it ourselves if necessary
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
Copy the code
In a nutshell:
- The Kotlinx-Coroutines-Android framework is mandatory and mainly provides a proprietary scheduler
- Anko-sdk27-coroutines is optional and provides a cleaner extension to some UI components, such as onClick, but it has its own problems, which we’ll explore in more detail later
- Anko-coroutines is for reference only. At the present stage (2019.4), we have not followed up the official version 1.3 coroutines, so try not to use them in the version after 1.3. The two methods provided are relatively simple, and can be modified and used by ourselves if necessary.
We’ve talked a lot about coroutines and how to use them, but we’ll give you a few practical tips on how to use coroutines on Android.
2. UI lifecycle scope
One of the things that Android developers often think about is making requests that automatically cancel when the current UI or Activity exits or is destroyed, and we’ve had various solutions to this problem with RxJava.
2.1 use MainScope
Coroutines have a natural property just enough to support this: scope. MainScope function is also provided officially. Let’s see how it can be used in detail:
val mainScope = MainScope()
launchButton.setOnClickListener {
mainScope.launch {
log(1)
textView.text = async(Dispatchers.IO) {
log(2)
delay(1000)
log(3)
"Hello1111"
}.await()
log(4)}}Copy the code
We found that it works just like any other CoroutineScope. Any coroutine started from the same instance called mainScope will follow its scope definition.
public fun MainScope(a): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code
The container is designed to handle exceptions from the top down, which is the same thing the Container is designed to handle with the Android Main scheduler. Therefore, unless the scheduler is explicitly declared in scope, the coroutine body is scheduled to execute on the main thread. So the above example runs as follows:
[main] 1 2019-04-29 06:51:00.657 D: [main] 1 2019-04-29 06:51:00.657 D: [2019-04-29 06:51:01.662 D: [2019-04-29 06:51:01.662 D: [2019-04-29 06:51:01.662 D: [2019-04-29 06:51:01.662 D: [main] 4Copy the code
If we trigger scope cancellation elsewhere immediately after the previous operation, the coroutine in that scope will no longer execute:
val mainScope = MainScope()
launchButton.setOnClickListener {
mainScope.launch {
...
}
}
cancelButton.setOnClickListener {
mainScope.cancel()
log("MainScope is cancelled.")}Copy the code
If we click the above two buttons in quick succession, the result is obvious:
[main] 1 2019-04-29 07:12:20.5d: [main] 1 2019-04-29 07:12:20.5d: [defaultDispatcher-worker-2] 2 2019-04-29 07:12:21.046 D: MainScope is cancelled.Copy the code
2.2 Constructing an abstract Activity with scope
Although we experimented with MainScope earlier and found it easy to control the cancellation of all coroutines in its scope, as well as the ability to seamlessly cut asynchronous tasks back to the main thread, these are all desirable features, but the writing is still not pretty.
The official recommendation is that we define an abstract Activity, for example:
abstract class ScopedActivity: Activity(), CoroutineScope by MainScope(){
override fun onDestroy(a) {
super.onDestroy()
cancel()
}
}
Copy the code
When the Activity exits, the corresponding scope is cancelled, and all requests made in the Activity are cancelled. When used, you just need to inherit this abstract class:
class CoroutineActivity : ScopedActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutine)
launchButton.setOnClickListener {
launch { // Call the ScopedActivity (MainScope) method directly. } } } suspendfun anotherOps(a)= coroutineScope { ... }}Copy the code
In addition to obtaining MainScope capabilities within the current Activity, you can also pass this Scope instance to other modules that need it. For example, a Presenter usually needs to have the same life cycle as an Activity, so you can pass this Scope if necessary:
class CoroutinePresenter(private val scope: CoroutineScope): CoroutineScope by scope{
fun getUserData(a){ launch { ... }}}Copy the code
In most cases, the Presenter method is also called by the Activity directly. Therefore, it is possible to render the Presenter method as the suspend method and then nest the scope with coroutineScope. Nested subscopes are also cancelled, thus cancelling all subcoroutines:
class CoroutinePresenter {
suspend fun getUserData(a)= coroutineScope { launch { ... }}}Copy the code
2.3 Provide scope for activities in a more friendly way
Abstract classes often break our inheritance, which can be very damaging to the development experience. Therefore, should we consider building an interface that can have scope and auto-cancel capabilities as long as the Activity implements this interface?
First we define an interface:
interface ScopedActivity {
val scope: CoroutineScope
}
Copy the code
It is our humble wish to implement this interface to automatically get scope, but how to implement the scope member? It is not ideal to leave it to the interface implementer, so we can implement it ourselves, because we are an interface, so we have to deal with it like this:
interface MainScoped {
companion object {
internal val scopeMap = IdentityHashMap<MainScoped, MainScope>()
}
val mainScope: CoroutineScope
get() = scopeMap[this as Activity]!!
}
Copy the code
The next thing to do is to create and remove the corresponding scopes where appropriate. We then define two methods:
interface MainScoped {...fun createScope(a){
// Or change to a lazy implementation, which will be created on time
val activity = this asActivity scopeMap[activity] ? : MainScope().also { scopeMap[activity] = it } }fun destroyScope(a){
scopeMap.remove(this asActivity)? .cancel() } }Copy the code
Since we need the Activity to implement this interface, we can simply enforce it. Of course, if we consider robustness, we can do some exception handling. Here, as an example, we only provide the core implementation.
Now, where do you do create and cancel? Obviously it with Application. The most appropriate ActivityLifecycleCallbacks:
class ActivityLifecycleCallbackImpl: Application.ActivityLifecycleCallbacks {.override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?). {
(activity as? MainScoped)? .createScope() }override fun onActivityDestroyed(activity: Activity) {
(activity as? MainScoped)? .destroyScope() } }Copy the code
All that’s left is to register this listener in the Application, which everyone knows, but I won’t show you the code.
Let’s see how it works:
class CoroutineActivity : Activity(), MainScoped {
override fun onCreate(savedInstanceState: Bundle?).{... launchButton.setOnClickListener { scope.launch { ... }}}}Copy the code
We can also add some useful methods to simplify the operation:
interface MainScoped {...fun <T> withScope(block: CoroutineScope. () -> T) = with(scope, block)
}
Copy the code
You can also write this in your Activity:
withScope { launch { ... }}Copy the code
Note that USES IdentityHashMap sample, this suggests that for the scope of reading and writing are thread safe, so don’t in other threads trying to obtain its value, unless you introduce a third party or to achieve a IdentityConcurrentHashMap, even so, Scope is also not designed to be accessed by other threads.
According to this idea, I provided a set of more complete solutions, which not only support activities but also support support-fragment versions of fragments above 25.1.0. Anko also provides some useful MainScope-based listener extensions that can be used with this framework:
api 'com. Bennyhuo. Kotlin: coroutines - android - mainscope: 1.0'
Copy the code
3. Use GlobalScope sparingly
3.1 What’s wrong with GlobalScope
We used GlobalScope in our previous examples, but GlobalScope does not inherit external scopes, so it is important to note that if you start the coroutine internally using GlobalScope after using MainScope for the binding life cycle, That means MainScope won’t work as well as it should.
The caution here is to be careful if you use constructors that have no dependent scope. For example, the onClick extension in Anko:
fun View.onClick(
context: CoroutineContext = Dispatchers.Main,
handler: suspend CoroutineScope. (v: View) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context, CoroutineStart.DEFAULT) {
handler(v)
}
}
}
Copy the code
Perhaps this is just a convenience, since onClick is written with fewer characters than setOnClickListener and has a more event-like name, but the hidden risk is that the coroutine started with onClick will not be cancelled when the Activity is destroyed. You need to think clearly about the risks.
Of course, the fundamental reason Anko does this is that OnClickListener doesn’t have a scope with life-cycle benefits. What if I can’t start coroutines without GlobalScope? Given our previous example, there are other ways to solve this problem:
interface MainScoped {...fun View.onClickSuspend(handler: suspend CoroutineScope. (v: View) -> Unit) {
setOnClickListener { v ->
scope.launch { handler(v) }
}
}
}
Copy the code
In the MainScoped interface defined above, we can get a life-cycle supported MainScope instance through scope, so we can start the coroutine directly to run OnClickListener. So the key here is how to get the scope.
I have defined such a listener for you in the framework, see 2.3.
3.2 Coroutine version AutoDisposable
Of course, there are other ways to ensure that coroutines are cancelled in time than directly starting them with an appropriate scope.
You must have used RxJava, and you must know that you sent a task with RxJava, the task is not finished before the page is closed, if the task does not come back, the page will be leaked; If the task comes back later, the callback to update the UI will also have a high probability of null Pointers.
AutoDispose, Uber’s open source framework, is sure to be used. It is actually using OnAttachStateChangeListener of View, when the View is added, we will cancel all before use RxJava sent request.
static final class Listener extends MainThreadDisposable implements View.OnAttachStateChangeListener {
private final View view;
private final CompletableObserver observer;
Listener(View view, CompletableObserver observer) {
this.view = view;
this.observer = observer;
}
@Override public void onViewAttachedToWindow(View v) {}@Override public void onViewDetachedFromWindow(View v) {
if(! isDisposed()) {// Do you see it?observer.onComplete(); }}@Override protected void onDispose(a) {
view.removeOnAttachStateChangeListener(this); }}Copy the code
We can also create onClickAutoDisposable for Anko extension onClick.
fun View.onClickAutoDisposable (
context: CoroutineContext = Dispatchers.Main,
handler: suspend CoroutineScope. (v: View) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context, CoroutineStart.DEFAULT) {
handler(v)
}.asAutoDisposable(v)
}
}
Copy the code
We know that launch launches a Job, so we can use asAutoDisposable to convert it to a Job that supports automatic cancellation:
fun Job.asAutoDisposable(view: View) = AutoDisposableJob(view, this)
Copy the code
So the implementation of AutoDisposableJob can be done by referring to the implementation of AutoDisposables:
class AutoDisposableJob(private val view: View, private val wrapped: Job)
// We implement the Job interface, but we don't implement its methods directly. Wrapped is used to delegate the interface
: Job by wrapped, OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?). = Unit
override fun onViewDetachedFromWindow(v: View?). {
// When the View is removed, remove the coroutine
cancel()
view.removeOnAttachStateChangeListener(this)}private fun isViewAttached(a)= Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken ! =null
init {
if(isViewAttached()) {
view.addOnAttachStateChangeListener(this)}else {
cancel()
}
// Remove the listener immediately after the execution of the coroutine to avoid leakage
invokeOnCompletion() {
view.removeOnAttachStateChangeListener(this)}}}Copy the code
In this case, we can use this extension:
button.onClickAutoDisposable{
try {
val req = Request()
val resp = async { sendRequest(req) }.await()
updateUI(resp)
} catch (e: Exception) {
e.printStackTrace()
}
}
Copy the code
When the button object is removed from the window, our coroutine receives the cancel instruction. Although the execution of the coroutine is not cancelled by the Activity’s onDestroy, it is tightly tied to the View’s click event. Even if the Activity is not destroyed, removing the View itself cancels the coroutine directly from the listener.
If you want to use this extension, I’ve already put it in jCenter for you to use:
api "Com. Bennyhuo. Kotlin: coroutines - android - autodisposable: 1.0"
Copy the code
Add to a dependency and use it.
4. Use the scheduler wisely
Using coroutines on Android is more about simplifying the writing of asynchronous logic and using scenarios more similar to RxJava. While using RxJava, I’ve seen a lot of developers just use the thread cutting feature, and since the RxJava thread cutting API is easy to use, it also causes a lot of mindless thread switching, which is actually not good. This is especially important when using coroutines, because the way coroutines switch threads is made simpler and more transparent by RxJava, which is a good thing, but it’s not being abused.
The preferred way to write this is that most of the UI logic is handled in the UI thread. Even if the UI uses dispatchers. Main to start the coroutine, if some IO operations are involved, use async to schedule them to Dispatchers.IO. When the result returns, the coroutine will help us cut back to the main thread — this is very similar to the single-threaded mode of working like Nodejs.
For uI-unrelated logic, such as batch offline data download tasks, the default scheduler is usually sufficient.
5. Summary
This article, mainly based on the theoretical knowledge we talked about in front, further to the specific combat perspective of Android migration, compared with other types of applications, Android as a UI program is the biggest characteristic of asynchronous UI to coordinate the life cycle, coroutine is no exception. Once we’re comfortable with the scoping rules of coroutines and how they relate to the UI lifecycle, we’ll be pretty comfortable with using coroutines.
Welcome to Kotlin Chinese community!
Chinese official website: www.kotlincn.net/
Official Chinese blog: www.kotliner.cn/
Public id: Kotlin
Zhihu column: Kotlin
CSDN: Kotlin Chinese Community
Nuggets: Kotlin Chinese Community
Brief: Kotlin Chinese Community