Kotlin Coroutines to perform asynchronous load example details

  • 2021-01-14 05:53:20
  • OfStack

preface

Kotlin Coroutines is the new asynchronous API introduced by Kotlin. It is not the best solution for all problems, but hopefully it will make things easier in many cases. Here is just a simple display of the specific use of this library in Android. The following words are not much to say, let's take a look at the detailed introduction.

The introduction of Coroutines


// in application the build.gradle In the file android Add the following code to the node 
kotlin {
 experimental {
  coroutines 'enable'
 }
}

// Add the following two lines to the dependency 
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20"

The first Coroutines example

Usually we load 1 image into ImageView, and the asynchronous load task is as follows:


fun loadBitmapFromMediaStore(imageId: Int, 
        imagesBaseUri: Uri): Bitmap {
 val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
 return MediaStore.Images.Media.getBitmap(contentResolver, uri)
}

This method must be executed in the background thread, because it belongs to an IO operation, which means that we have a number of solutions to start the background task. Once the method returns an bitmap, we need to display it in Imageview immediately.


imageView.setImageBitmap(bitmap)

This line of code must be executed on the main thread, otherwise it will be crash.

Depending on the proper thread selection, the above 3 lines of code will cause the program to freeze or flash back if written to 1. Let's take a look at how Coroutines uses kotlin to solve this problem:


val job = launch(Background) {
 val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
 val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, 
 launch(UI) {
 imageView.setImageBitmap(bitmap)
 }
}

The most important ones are launch() and Background and UI. ES37en () means to create and start an Coroutine. The Background parameter CoroutineContext is used to ensure that the application is executed in the background thread, so that the application will not get stuck or flash back.


internal val Background = newFixedThreadPoolContext(2, "bg")

This will create a new context and use two regular threads while performing his task.

Next launch(UI), this will trigger another coroutine, which will be executed on Android
The main thread of.

Can be cancelled

The next challenge is dealing with the ES56en declaration cycle, where you leave the ES57en when you are loading a task and it is not yet finished, so that it is calling imageView.setImageBitmap(bitmap) So we need to cancel the task before we leave the activity. Here we use the return value of the launch() method. When activity calls the onStop method, we need to use job to cancel the task


job.cancel()

This is just like calling dispose when you use Rxjava and cancel when you use AsyncTask.

LifecycleObserver

Android Architecture Components provides Android developers with a number of particularly powerful libraries, one of which is Lifecycle API. To provide us with an easy way to monitor the activity and fragment life cycle in real time, we define the code below to use with coroutines1.


class CoroutineLifecycleListener(val deferred: Deferred<*>) : LifecycleObserver {
 @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
 fun cancelCoroutine() {
 if (!deferred.isCancelled) {
  deferred.cancel()
 }
 }
}

Let's create an extension function for LifecycleOwner:


fun <T> LifecycleOwner.load(loader: () -> T): Deferred<T> {
 val deferred = async(context = Background, 
      start = CoroutineStart.LAZY) {
 loader()
 }
 lifecycle.addObserver(CoroutineLifecycleListener(deferred))
 return deferred
}

There are a lot of new things in this method, which is explained in the following 11:

Now we can call it in one activity or fragment load() , and access the lifecycle member from this function, and add our CoroutineLifecycleListener as the observer.

load method requires a loader as parameters and returns a generic type T, I in the load method, we call the creator of the additional 1 Coroutine async () function, will use Background coroutine context on a mission in the background thread, pay attention to this method there is another one parameter start = CoroutineStart. LAZY, this means that coroutine not executed immediately, until is invoked.

coroutine will then return 1 Defered<T> Object to the caller. This is similar to our previous Job, but it can also carry a delay value, such as JavaScript Promise in regular Java API or JavaScript Future <T> Better yet, he has an await method.

Next we define another extension function then() , this time in Deferen<T> The above definition is the type returned by our above load method, which also takes 1 lambda as an argument, named block, and it takes a single object of type T as its argument.


infix fun <T> Deferred<T>.then(block: (T) -> Unit): Job {
 return launch(context = UI) {
 block(this@then.await())
 }
}

This function will use launch() The function creates another Coroutine, this time running on the main thread. The lambda (named block) passed to this Coroutine takes as its argument the value of the completed Deferred object. We call await() To suspend execution of the Coroutine object until the Deferred object returns a value.

Here's where coroutine becomes so impressive. await() The call is made on the main thread, but does not block further execution of that thread. It will simply pause the execution of the function until it is ready, when it resumes and passes the delay value to lambda. When coroutine is paused, the main thread can continue doing other things. The await function is one of the core concepts in coroutine, what makes the whole thing so magical.

load() The life cycle observer added to the function will be called on our activity load()0 Then cancel the first coroutine. This will also cause the second coroutine to be cancelled, blocked block() Is invoked.

Kotlin Coroutine DSL

Now that we have two extension functions and a class that will handle coroutine cancellation, let's see how to use them:


load {
 loadBitmapFromMediaStore(imageId, imagesBaseUri)
} then {
 imageView.setImageBitmap(it)
}

In the above code, we pass the lambda method to the load function, which calls the loadBitmapFromMediaStore method, which must be executed on the background thread until the method returns one Bitmap. The return value of the load method is Deferred<Bitmap> .

As an extension function, then() Method is declared using infix, although the load method returns Deferred<Bitmap> , but one bitmap return value will be passed to the then method, so we can call it directly from within the then method imageView.setImageBitmap(it) .

The code above can be used for any asynchronous call that needs to happen on a background thread and where the return value should be returned to the main thread, as in the example above. It doesn't write multiple calls like RxJava, but it's easier to read and probably covers many of the most common cases. Now you can do this safely without worrying about causing context leaks or handling threads on every call;


fun loadBitmapFromMediaStore(imageId: Int, 
        imagesBaseUri: Uri): Bitmap {
 val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
 return MediaStore.Images.Media.getBitmap(contentResolver, uri)
}
0

The then() and load() methods are just the tip of the iceberg for this new library, but I do expect something similar in the future Kotlin-based Android library, with the 1-d1 coroutine version reaching a stable version. Until then, you can use or modify the code above, or check out Anko Coroutines. I've also released a more complete version on GitHub. (https: / / github com/ErikHellman KotlinAsyncWithCoroutines download (local)).

conclusion


Related articles: