Cracking Kotlin coroutines (8) - Android articles
Keywords: Kotlin Coroutine Android Anko
It is actually very easy to use coroutines instead of callbacks or RxJava on Android, and we can even control the execution status of coroutines in a larger range in combination with the life cycle of the UI~
1. Configuration dependencies
We have mentioned that if we develop on Android, we need to introduce
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutine_version'
copy
This framework contains Android-specific Dispatcher
, we can get this instance Dispatchers.Main
through ; it also contains MainScope
, used to combine with the Android scope.
Anko also provides some more convenient methods, such as onClick
and so on , if necessary, you can also introduce its dependencies:
//提供 onClick 类似的便捷的 listener,接收 suspend Lambda 表达式
implementation "org.jetbrains.anko:anko-sdk27-coroutines:$anko_version"
//提供 bg 、asReference,尚未没有跟进 kotlin 1.3 的正式版协程,不过代码比较简单,如果需要可以自己改造
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
copy
simply put:
- The kotlinx-coroutines-android framework is a must, and it mainly provides a dedicated scheduler
- anko-sdk27-coroutines is optional and provides some more concise extensions of UI components, such as onClick, but it also has its own problems, which we will discuss in detail later
- anko-coroutines is for reference only. At this stage (2019.4), since the official version 1.3 of coroutines has not been followed up, try not to use it in versions after 1.3. The two methods provided are relatively simple. If necessary, you can modify and use them yourself.
We have already discussed a lot about the principle and usage of coroutines. Regarding the use of coroutines on Android, we only give a few practical suggestions.
2. UI life cycle scope
One thing that Android development often thinks of is to make the outgoing request automatically cancel when the current UI or Activity exits or is destroyed. We have also used various solutions to solve this problem when using RxJava.
2.1 Using MainScope
Coroutines have a very natural feature that is just enough to support this, and that is scope. The official also provides MainScope
this function, let's see how to use it:
val mainScope = MainScope()
launchButton.setOnClickListener {
mainScope.launch {
log(1)
textView.text = async(Dispatchers.IO) {
log(2)
delay(1000)
log(3)
"Hello1111"
}.await()
log(4)
}
}
We found that it is actually no different from other CoroutineScope
usages . mainScope
The coroutines started through the same instance called will follow its scope definition, so MainScope
what is the definition of ?
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
It turned out to SupervisorJob
be Dispatchers.Main
just integrated, and its exception propagation is top-down, which is consistent with the behavior supervisorScope
of , in addition, the scheduling in the scope is based on the scheduler of the Android main thread, so unless the scheduler is explicitly declared in the scope, the coordination The program body is scheduled to execute on the main thread. So the result of running the above example is as follows:
2019-04-29 06:51:00.657 D: [main] 1
2019-04-29 06:51:00.659 D: [DefaultDispatcher-worker-1] 2
2019-04-29 06:51:01.662 D: [DefaultDispatcher-worker-2] 3
2019-04-29 06:51:01.664 D: [main] 4
If we trigger the cancellation of the scope elsewhere immediately after triggering the previous operation, then the coroutines in the scope will not continue to execute:
val mainScope = MainScope()
launchButton.setOnClickListener {
mainScope.launch {
...
}
}
cancelButton.setOnClickListener {
mainScope.cancel()
log("MainScope is cancelled.")
}
If we quickly click on the two buttons above in sequence, the result is obvious:
2019-04-29 07:12:20.625 D: [main] 1
2019-04-29 07:12:20.629 D: [DefaultDispatcher-worker-2] 2
2019-04-29 07:12:21.046 D: [main] MainScope is cancelled.
2.2 Construct an abstract Activity with scope
Although we have experienced it before MainScope
and found that it can easily control the cancellation of all coroutines within its scope, and seamlessly switch asynchronous tasks back to the main thread. These are the features we want, but the writing is still not beautiful enough.
The official recommendation is that we define an abstract one Activity
, for example:
abstract class ScopedActivity: Activity(), CoroutineScope by MainScope(){
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
In this way, when Activity
the exits , the corresponding scope will be cancelled, and all Activity
requests initiated in the will be cancelled. When using, you only 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 { // 直接调用 ScopedActivity 也就是 MainScope 的方法
...
}
}
}
suspend fun anotherOps() = coroutineScope {
...
}
}
In addition to the capabilities obtained within the Activity
current MainScope
, you can also pass this Scope instance to other modules that need it. For example, Presenter
it usually needs to Activity
maintain the same life cycle as , so you can also pass this scope if necessary:
class CoroutinePresenter(private val scope: CoroutineScope): CoroutineScope by scope{
fun getUserData(){
launch { ... }
}
}
In most cases, Presenter
the method of will also be called Activity
directly , so Presenter
the method of can also be created suspend
as a method , and then use coroutineScope
the nested scope, so that after MainScope
the is cancelled, the nested sub-scopes will also be cancelled, thereby achieving the cancellation of all The purpose of sub-coroutines:
class CoroutinePresenter {
suspend fun getUserData() = coroutineScope {
launch { ... }
}
}
2.3 Provide scope for Activity more friendly
Abstract classes often break our inheritance system, which is still very harmful to the development experience. Therefore, can we consider constructing an interface, as long as this interface Activity
is implemented cancel?
First we define an interface:
interface ScopedActivity {
val scope: CoroutineScope
}
We have a simple wish that we can automatically obtain the scope by implementing this interface, but the question is, how to realize this scope
member ? It is obviously not ideal to leave it to the interface implementer. Let’s implement it ourselves. Since we are an interface, we can only deal with it like this:
interface MainScoped {
companion object {
internal val scopeMap = IdentityHashMap<MainScoped, MainScope>()
}
val mainScope: CoroutineScope
get() = scopeMap[this as Activity]!!
}
The next thing is to create and cancel the corresponding scope when appropriate. We then define two methods:
interface MainScoped {
...
fun createScope(){
//或者改为 lazy 实现,即用到时再创建
val activity = this as Activity
scopeMap[activity] ?: MainScope().also { scopeMap[activity] = it }
}
fun destroyScope(){
scopeMap.remove(this as Activity)?.cancel()
}
}
Because we need Activity
to implement this interface, we can just force it directly. Of course, if we consider robustness, we can do some exception handling. Here we only provide the core implementation as an example.
The next step is to consider where to complete the creation and cancellation? Obviously this is Application.ActivityLifecycleCallbacks
the :
class ActivityLifecycleCallbackImpl: Application.ActivityLifecycleCallbacks {
...
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
(activity as? MainScoped)?.createScope()
}
override fun onActivityDestroyed(activity: Activity) {
(activity as? MainScoped)?.destroyScope()
}
}
All that's left is to register the listener Application
in , which everyone knows, so I won't give the code.
Let's see how to use it:
class CoroutineActivity : Activity(), MainScoped {
override fun onCreate(savedInstanceState: Bundle?) {
...
launchButton.setOnClickListener {
scope.launch {
...
}
}
}
}
We can also add some useful methods to simplify this operation:
interface MainScoped {
...
fun <T> withScope(block: CoroutineScope.() -> T) = with(scope, block)
}
In this Activity
way it can also be written like this:
withScope {
launch { ... }
}
Note that it is used in the example
IdentityHashMap
, which indicates that the reading and writing of scope is not thread-safe, so don't try to get its value in other threads, unless you introduce a third party or implement one yourselfIdentityConcurrentHashMap
, even so, itscope
is Should be accessed from other threads.
According to this idea, I provide a more complete solution, which not only supports Activity
but also support-fragment versions above 25.1.0 Fragment
, and provides some MainScope
useful , source address: kotlin- coroutines-android (https://github.com/enbandari/kotlin-coroutines-android), introduce this framework to use:
api 'com.bennyhuo.kotlin:coroutines-android-mainscope:1.0'
3. Use GlobalScope with caution
3.1 What's wrong with GlobalScope
We used it often in previous examples GlobalScope
, but GlobalScope
will not inherit the external scope, so you must pay attention when using it. If MainScope
after using the binding life cycle, use internally to GlobalScope
start the coroutine, it means that MainScope
it will not play its due role. role.
The thing to be careful here is that if you use some constructors without dependent scope, you must be careful. For example onClick
the extension :
fun View.onClick(
context: CoroutineContext = Dispatchers.Main,
handler: suspend CoroutineScope.(v: View) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context, CoroutineStart.DEFAULT) {
handler(v)
}
}
}
Maybe we are just trying to make it easier. After all, onClick
it setOnClickListener
takes a lot less characters to write than , and the name also looks more like an event mechanism, but the hidden risk is that the coroutines onClick
started will not Activity
be canceled with the destruction of . The risks need to be considered clearly by yourself.
Of course, the fundamental reason why Anko will do this is that there is no scope with life cycle blessing OnClickListener
at all . GlobalScope
The coroutine cannot be started without using , what should I do? Combined with the example we gave earlier, there is actually a completely different solution to this matter:
interface MainScoped {
...
fun View.onClickSuspend(handler: suspend CoroutineScope.(v: View) -> Unit) {
setOnClickListener { v ->
scope.launch { handler(v) }
}
}
}
In MainScoped
the interface , we can scope
get MainScope
the instance , then use it directly to start the coroutine to run OnClickListener
. The problem will be solved. So the key point here is how to get the scope.
I have defined such a listener in the framework for everyone, please refer to 2.3.
3.2 Coroutine version of AutoDisposable
Of course, in addition to directly using a suitable scope to start the coroutine, we have other ways to ensure that the coroutine is canceled in time.
You must have used RxJava, and you must know that when you send a task with RxJava, the page will be closed before the task ends. If the task does not come back for a long time, the page will be leaked; if the task comes back later, execute the callback to update the UI. Sometimes there is a high probability of a null pointer.
Therefore, everyone will definitely use Uber's open source framework AutoDispose (https://github.com/uber/AutoDispose). It is actually View
used OnAttachStateChangeListener
. When View
is taken down, we cancel all the requests sent by RxJava before.
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()) {
//看到没看到没看到没?
observer.onComplete();
}
}
@Override protected void onDispose() {
view.removeOnAttachStateChangeListener(this);
}
}
Considering the problem that the Anko extension mentioned above onClick
cannot cancel the coroutine, we can also make one onClickAutoDisposable
.
fun View.onClickAutoDisposable (
context: CoroutineContext = Dispatchers.Main,
handler: suspend CoroutineScope.(v: View) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context, CoroutineStart.DEFAULT) {
handler(v)
}.asAutoDisposable(v)
}
}
We know launch
that will start one Job
, so we can convert it to a type that supports autocancellation asAutoDisposable
with :
fun Job.asAutoDisposable(view: View) = AutoDisposableJob(view, this)
copy
Then the implementation AutoDisposableJob
of just refer to the implementation of AutoDisposable and do the same:
class AutoDisposableJob(private val view: View, private val wrapped: Job)
//我们实现了 Job 这个接口,但没有直接实现它的方法,而是用 wrapped 这个成员去代理这个接口
: Job by wrapped, OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) = Unit
override fun onViewDetachedFromWindow(v: View?) {
//当 View 被移除的时候,取消协程
cancel()
view.removeOnAttachStateChangeListener(this)
}
private fun isViewAttached() =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken != null
init {
if(isViewAttached()) {
view.addOnAttachStateChangeListener(this)
} else {
cancel()
}
//协程执行完毕时要及时移除 listener 免得造成泄露
invokeOnCompletion() {
view.removeOnAttachStateChangeListener(this)
}
}
}
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()
}
}
When button
this object is removed from the window, our coroutine will receive the cancel instruction. Although in this case the execution of the coroutine will not beActivity
canceled following the , it is closely integrated with the click event of , even if there is no When it is destroyed, it will directly cancel the monitoring coroutine when it is removed.onDestroy
View
Activity
View
If you want to use this extension, I have put it in jcenter for you, use it directly:
api "com.bennyhuo.kotlin:coroutines-android-autodisposable:1.0"
copy
Add it to the dependencies and use it. The source code is also here: kotlin-coroutines-android (https://github.com/enbandari/kotlin-coroutines-android), don't forget to star.
4. Reasonable use of the scheduler
Using coroutines on Android is more about simplifying the writing of asynchronous logic, and the usage scenarios are more similar to RxJava. When using RxJava, I found that many developers only use its thread cutting function, and because the RxJava thread cutting API itself is simple and easy to use, it will also cause a lot of brainless thread switching operations, which is actually not good. Then you should pay more attention to this problem when using coroutines, because the way of coroutines switching threads is more concise and more transparent by RxJava. This is a good thing, but it is afraid of being abused.
The recommended writing method is that most of the UI logic is processed in the UI thread. Even if it is used Dispatchers.Main
to , if some io operations are involved, use async
to schedule it Dispatchers.IO
on , and the coroutine will help us switch when the result is returned. Back to the main thread - this is very similar to the single-threaded working mode of Nodejs.
For some UI-independent logic, such as batch offline data download tasks, the default scheduler is usually sufficient.
5. Summary
This article is mainly based on the theoretical knowledge we mentioned earlier, and further migrates to the specific practical perspective of Android. Compared with other types of applications, the biggest feature of Android as a UI program is that it needs to coordinate the life cycle of the UI asynchronously. Cheng is no exception. Once we are familiar with the scope rules of coroutines and the relationship between coroutines and UI life cycle, I believe that everyone will be handy when using coroutines.
Kotlin coroutine learning materials can be obtained for free by scanning the QR code below!
"The most detailed Android version of kotlin coroutine entry advanced combat in history"
Chapter 1 Introduction to the Basics of Kotlin Coroutines
● 协程是什么
● 什么是Job 、Deferred 、协程作用域
● Kotlin协程的基础用法
Chapter 2 Preliminary Explanation of Key Knowledge Points of Kotlin Coroutine
● 协程调度器
● 协程上下文
● 协程启动模式
● 协程作用域
● 挂起函数
Chapter 3 Exception Handling of Kotlin Coroutines
● 协程异常的产生流程
● 协程的异常处理
Chapter 4 Basic application of kotlin coroutines in Android
● Android使用kotlin协程
● 在Activity与Framgent中使用协程
● ViewModel中使用协程
● 其他环境下使用协程
Chapter 5 Network request encapsulation of kotlin coroutine
● 协程的常用环境
● 协程在网络请求下的封装及使用
● 高阶函数方式
● 多状态函数返回值方式