قالب وردپرس درنا توس
Home / IOS Development / Kotlin Coroutine's Android Guide: Advanced

Kotlin Coroutine's Android Guide: Advanced



In this advanced Kotlin Coroutines tutorial for Android, you will gain a deeper understanding of Kotlin Coroutines by replacing common asynchronous programming methods, such as creating new Thread s and using callbacks, in an Android app.

You will be working on a modified version of the RWDC2018 launch project from the Android Background Processing video course developed by Joe Howard. For more detailed coverage of Kotlin Coroutines, see Kotlin Coroutines by Tutorials by Filip Babić and Nishant Srivastava.

Note : This tutorial assumes that you have experience with Android and Kotlin. But if not, check out the beginnings of Android development with the Kotlin series and other Kotlin and Android tutorials RayWenderlich.com has to offer.

What are Coroutines?

Now you have read a few articles and blog posts about Kotlin Coroutines. You're thinking "not another definition of coroutine!" Well, although this is not a Getting Started post, it is still best to understand the history of a topic before deciding on a definition.

Besides, you might learn something new. :]

Note : If you are already familiar with coroutines in general and have read the official documentation, you may find this explanation superfluous. If so, skip this section and skip to code immediately.

The Origins

Coroutines is not a new concept. In fact, Melvin Conway, a mathematician, physicist, and computer scientist coined the term coroutines in his article, "Design of a Separable Transition-Diagram Compiler" in 1958. His paper suggested "organizing a compiler as a set of coroutines, which provides the ability to use separate passports in debugging and then run a single pass compiler in production. "

Coroutines were first implemented as assembly language methods, and then implemented in high-level languages ​​such as C, C ++, C #, Clojure, Java, JavaScript, Python, Ruby, Perl, Scala and, of course, Kotlin.

So, what are coroutines?

Today

Kotlin Evolution and Enhancement Process, or KEEP, the GitHub repository provides a more complete definition, stating that a coroutine is a "instance of suspendable computation." This is conceptually similar to a thread because it uses a block block to run and has a similar life cycle.

KEEP states further that a coroutine is "created and started, but it is not tied to any particular thread. It can stop execution in one thread and continue in another. Furthermore, as a future or a promise it can complete with a result (which is either a value or an exception). "

In other words, Coroutines attenuates the complications of working with asynchronous programming. The code you enter is sequential. This makes it easier to understand than callbacks and various observable constructions.

Threads are expensive to make and require resources to maintain. That means there are only so many threads you can create in a system. Conversely, coroutines manage their own pool of threads. Some broadcasters even share pools. A suspended coroutine does not block any threads. It is waiting for the next available thread to resume.

By disconnecting work and threads, it is possible to create and execute thousands of coroutines. This is in a limited pool of wires and without any overhead.

In short, a coroutine is a code component with a life cycle that is not bound to a single thread. Any thread in the pool can perform, suspend and resume the coroutine.

Getting Started

This tutorial is a bit unconventional when it comes to the code you are going to work with. First, you will experiment with a few concepts and important components of coroutines in Kotlin Playground . Then switch to an Android app project where you want to add a lot of advanced use of coroutine.

Key Components

These are the most commonly used Kotlin Coroutine components when implementing coroutines in an Andriod app.

Suspendable Functions

Coroutines operate according to the principle of suspendable functions . As you have already learned, coroutines can pause and resume at any time between any number of threads. This process is called code suspension .

It allows coroutines to be light and fast because they do not actually distribute anything overhead, such as threads. Instead, they use predefined resources and smart resource management.

The system uses continuations to know when and where to resume a function.

Continued

When a function suspends, there is information or status of the suspended coroutine. Each time a coroutine suspends, it stores the state in a sequel. When the coroutine resumes, the continuation contains enough information to continue the rest of the coroutine seamlessly.

Continued interface consists of a CoroutineContext and a final callback used to report success or failure in the coroutine. In the code snippet below, an existing asynchronous API service that uses callbacks is wrapped in a suspendable function, and it mediates the result or error using a Continuation . It's just a sample feature, but the idea is there.

  suspend fun  suspendAsyncApi (data: Data): Result =
suspendCancellableCoroutine {continued ->
apiService.doAsyncStuff  (data,
{result -> continuation.resume (result)}, // resume with a result
{error -> continuation.resumeWithException (error)} // resume with an error
)
}

You can see how by abstracting the function's return value, with a coroutine and Continuation you can return a value without actually returning it immediately. You package the asynchronous callback API into a suspendable function which, when called, will act as a sequential code. If you called this function from another coroutine, it would look like this:

  val username = suspendAsyncApi  ("userId") // get the username of a given user ID

This is not a real API, but you can actually write your own API, which works like this. The important part is how coroutines and continuations bridge asynchronous and synchronous worlds while keeping the syntax clear.

Coroutine Context

Coroutine context is a persistent set of data about the coroutine. It is contained in Continuation making it an unchangeable collection of thread-local variables and program state associated with the coroutine.

Since coroutines are light, coroutine context is not a constraint. If the coroutine context needs to be changed, you can simply start a new coroutine, with a mutated context.

Coroutine Builders

To start and run new coroutines, you must use a Coroutine Builder . They take some code and wrap it in a coroutine, and send it to the system for execution. This makes them bread and butter from coroutines.

The main builder for coroutines is launch () . It creates a new coroutine and launches it immediately by default. It builds and launches a coroutine in the context of some CoroutineScope :

  GlobalScope.launch {// CoroutineScope
// coroutine body
}

Once you have obtained a CoroutineScope you can use launch () on it to start a coroutine. You can use coroutine builders in a normal non-suspending feature, or other suspendable features, starting with nested coroutines.

Simultaneous execution

Another coroutine builder is async () . It is special because you can use it to return a value from a coroutine, enabling simultaneous execution. You will use async () from any coroutine, such as:

  GlobalScope.launch {// CoroutineScope
val someValue = async {getValue ()} // value calculated in a coroutine
}

However, you cannot use the value yet. async () returns a Deferred which is a non-blocking cancelable future. To obtain the result, call wait () . When you start to wait, you suspend the wrapping of coroutine until you get the calculated value.

Blocking Builder

There is another builder you can use for coroutines, which is a bit unconventional. runBlocking () forces coroutines to block calls.

Note : runBlocking is a builder that blocks the thread until execution is completed to avoid JVM shutdown in special situations such as main features or tests. You should avoid using it in regular Kotlin coroutine code.

To explain how to start and execute Kotlin coroutines, it is best to take a look at some live code snippets:

  import kotlinx.coroutines. *
import java.lang.Thread

funny main () {
GlobalScope.launch {// launch new coroutine in the background and continue
delay (1000L) // non-blocking delay for 1 second (default time unit is ms)
println ("World!") // print after delay
val sum1 = async {// not blocking sum1
delay (100L)
2 + 2
}
val sum2 = async {// not blocking sum2
delay (500L)
3 + 3
}
println ("pending simultaneous sums")
val total = sum1.await () + sum2.await () // execution stops until both sums are calculated
println ("Total is: $ total")
}
println ("Hi,") // main thread continues while running coroutine
Thread.sleep (2000L) // blocks the main thread for 2 seconds to keep JVM alive
}

Run an example of Coroutine Builder in a Kotlin playground.

The excerpt above launches a Kotlin coroutine that uses delay () to stop the function for one second. Since Kotlin coroutines does not block any threads, the code of the other println () continues and prints Hi, .

Next, the code sleeps the main thread so that the program is not completed until the coroutine completes the execution. The Coroutine runs its second line and prints The World! .

It then builds and starts two asynchronous coroutines. Finally, when both concurrent operations are completed, it prints the total.

This is a simple yet effective way to learn about Kotlin coroutines and the idea behind it.

Look at the return type shooting () . It returns a Job that represents the calculation that you wrapped in a coroutine. You can nest jobs and create a hierarchy for children and parents.

You will see how to use this to interrupt coroutines in a later excerpt.

One of the things you used above is the GlobalScope instance for the coroutine scope. Let's see what the scope is and how to approach them.

CoroutineScope

CoroutineScope s limits new coroutines by providing a life cycle bound component that binds to a coroutine. Each coroutine builder is an extension function defined by the type CoroutineScope . launch () is an example of a corout builder.

You have already used GlobalScope . It is useful for top-level coroutines that work throughout the life of the app and are not tied to any lifecycle. Typically, you will use CoroutineScope over GlobalScope in an Android app to control when life-cycle events occur.

In an Android app, you implement CoroutineScope s on components with well-defined life cycles. These components include Activity Fragment and ViewModel .

Calling launch () on CoroutineScope s provides a Job that encloses a code block. When the scope is interrupted, all Kotlin's coroutines within the resources clear and interrupt.

Take the following code snippet:

  import kotlinx.coroutines. *

fun main () = runBlocking {// this: CoroutineScope
launch {
delay (200L)
println ("Task from runBlocking")
}

coroutineScope {// Creates a new coroutine scope
select job = launch {
println ("Task from nested launch, this is printed")
delay (500L)
println ("Nested launch task, this will not be printed")
}

delay (100L)
println ("Task from first coroutine scope") // Printed before the first launch
job.cancel () // This cancels the run of the run
}

println ("Coroutine scope is over") // This does not print until the nested launch is completed / canceled
}

Run an example of CoroutineScope at a Kotlin playground. When you examine the excerpt above, you see a few things.

First, force the coroutines to block, so you don't have to sleep the program like you did before. Then you launch a new coroutine that has a first delay. After that, use coroutineScope () to create a new scope. Then you launch a coroutine in the rescuer of the returned Job .

Because you postpone the first launch () it will not run until coroutineScope () completes completely. However, within coroutineScope () you save and delay the job and the nested coroutine. Since you cancel it after it is delayed, it will only print the first sentence, eventually canceling before the second statement. And when the coroutineScope () finishes, the first shootout () ends and it can proceed with execution.

Finally, when the scope is complete, runBlocking () can also be completed. This ends the program. It is important to understand this flow of execution, to build stable coroutines, without running conditions or hanging resources.

Job

In the previous section you saw how to cancel the execution of a coroutine. You should understand that a job is an interruptible component with a life cycle.

Jobs are usually created by calling launch () . You can also create them using a constructor – Job () . They can live within the hierarchy of other jobs either as parents or children. If you cancel a parent Job you will also cancel all its children.

If a child Job fails, or cancels, the parent and parent hierarchy will also cancel. The exception the hierarchy gets is, of course, a CancellationException .

Note : There is a special type Job that does not interrupt if one of its children fails – Supervisor Job . You can check it at the official documentation.

By default, a child's failure will cancel the parents and other children in the hierarchy. Sometimes you have to wait for a coroutine execution to be interrupted. In that case, you can call job.cancelAndJoin () instead of job.cancel () .

  import kotlinx.coroutines. *

fun main () = runBlocking {
val startTime = System.currentTimeMillis ()
val job = launch (Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) {// interruptible computation loop
// print a message twice a second
if (System.currentTimeMillis ()> = nextPrintTime) {
println ("I sleep $ {i ++} ...")
nextPrintTime + = 500L
}
}
}
delay (1300L) // delay a bit
println ("main: I'm tired of waiting!")
job.cancelAndJoin () // cancels the job and waits for it to complete
println ("main: Now I can quit.")
}

Run the interruptible CoroutineScope example in a Kotlin playground.

Production for the program will be a few prints from while the loop, then with interrupt and finally main () finishing.

There are advantages to being able to cancel a coroutine in an Android app. For example, say that an app goes in the background and that an activity stops. In this case, you should cancel all long-standing API calls to clean up the resources. This will help you avoid possible memory leaks or unwanted behavior.

You can cancel a job, with any child, from a Activity such as onStop () . And it's even easier if you do it by using CoroutineScope but you want to do it later.

CoroutineDispatchers

Forwarders determine which thread or thread pool the coroutine uses for execution. The coordinator can restrict a coroutine to a specific thread. It can also send it to a wire pool. Less often, it may allow a coroutine to run uncontrollably, without a specific threading rule, which can be unpredictable.

Here are some common transmitters:

    Dispatchers.Main: This transmitter limits coroutines to the main thread for UI-powered applications, such as Swing, JavaFX, or Android apps. It is important to note that this transmitter does not work without adding an environment-specific Main dispatcher dependency to Gradle or Maven.

    Use Dispatchers.Main.immediate for optimal UI performance on updates.

    Dispatchers.Default: This is the default coordinator used by default builders. It is supported by a shared selection of JVM threads. Use this transmitter for CPU-intensive calculations.

    Dispatchers.IO: Use this transmitter for I / O intensive blocking tasks using a shared group of threads.

    Dispatchers.Unconfined: This transmitter does not limit coroutines to any specific thread. Couroutine starts the execution of the inherited CoroutineDispatcher who called it. But after a suspension is over, it can continue in other threads.

    This lack of confinement can cause a coroutine destined for background expedition to run on the main thread, so use it sparingly.


  import kotlinx.coroutines. *

fun main () = runBlocking  {
launch {// context of parent, main runBlocking coroutine
println ("main runBlocking: I work in thread $ {Thread.currentThread (). name}")
}
launch (Dispatchers.Unconfined) {// not confined - runs right in the main thread, but not after suspension
println ("Unconfined: I work in thread $ {Thread.currentThread (). name}")
delay (100L) // delays (stops) execution 100 ms
println ("Unconfined: I work in thread $ {Thread.currentThread (). name}")
}
launch (Dispatchers.Default) {// will be sent to StandardDispatcher
println ("Default: I work in thread $ {Thread.currentThread (). name}")
}
launch (newSingleThreadContext ("MyOwnThread")) {// will get its own new thread
println ("newSingleThreadContext: I work in thread $ {Thread.currentThread (). name}")
}
}

Run an example of CoroutineDispatcher at a Kotlin playground. The print order is changed per execution at the playground.

You will see how each of the broadcasts prints its own context – its own thread. Furthermore, you can see how to create your own single thread contexts if you need a specific thread for a little coroutine.

Exception Handling

At JVM, threads are the core of Kotlin coroutine's machinery. JVM has a well-defined way of handling terminated threads and non-caught exceptions.

If an unspoken exception occurs in a thread, JVM will query the thread for a UncaughtExceptionHandler . JVM then passes the closing thread and the unspoken exception. This is important because coroutines and Java concurrency have the same exception behavior.

Coroutine builders fall into two exception categories. The first one propagates automatically, like the launch () so if bad things happen, you'll know it soon. The second reveals exceptions for the user to handle, such as async () . They will not propagate until you call and wait () to get the value.

In Android, builders who propagate exceptions also depend on Thread.UncaughtExceptionHandler . It is installed as a global coroutine exception handler. However, coroutine builders allow the user to provide a CoroutineExceptionHandler to have more control over how to handle exceptions.

  import kotlinx.coroutines. *

fun main () = runBlocking  {
// propagate exceptions from the default thread.UncaughtExceptionHandler
val job = GlobalScope.launch {
throw AssertionError ()
}

// blocks thread execution until coroutine is complete
job.join ()

// launches async coroutine, but the exception is not propagated until expected
val deferred = GlobalScope.async (Dispatchers.Default) {
throw AssertionError ()
}

// defines a specific handler
val handler = CoroutineExceptionHandler {_, exception ->
println ("We caught $ exception")
}

// propagating exceptions using a custom CoroutineExceptionHandler
GlobalScope.launch (handler) {
throw AssertionError ()
}

// This exception is finally propagated call waiting and should be handled by user e.g. with trial {} catch {}
deferred.await ()
}

Run examples with exception handling on a Kotlin Playground.

You should see a bug that gets caught immediately. After that, comment on the first throw clause. You should once again see an exception thrown, but this time from async () . If you comment on wait () CoroutineExceptionHandler captures the exception and prints which exception happened.

Knowing this, there are three ways to handle exceptions. First use try / catch in a launch () when you do not have a custom exception handler. The second is by wrapping pending () calls in a sample / catch block. And the last thing is to use an exception handler, to provide a place to catch exceptions.

Time to Code

You will be working on a modified version of the RWDC2018 app from the Android Background Processing video course developed by Joe Howard. The modified app only shows images taken at RWDevCon 2018.

The app retrieves these images using background threads. You will replace the implementation of background threads with Kotlin Coroutines.

Download Project

Download the start and end projects by clicking the Download Materials button at the top or bottom of this tutorial. Then import the startup project into Android Studio. Take the time to get to know the structure of the project.

 coroutines-start-1

Then go to PhotosRepository.kt in Android Studio. This class contains the thread code for downloading the banner and images for RecyclerView .

The images are downloaded into a background thread. You then save the results to LiveData using postValue () . postValue () updates the data on the main thread.

 coroutines-start-2

Adding dependencies

You must add Kotlin Coroutine dependencies to the app module. First, open build.gradle in the app module and add the following dependencies:

  • & # 39; org.jetbrains.kotlinx: kotlinx-coroutines-core: 1.2.1 & # 39; : Core priority for working with coroutines, such as builders, broadcasters, and suspend features.
  • & # 39; org.jetbrains.kotlinx: kotlinx-coroutines-android: 1.2.1 & # 39; : Offers dispatchers.Main context for Android applications.

To help you with that, copy and paste the following excerpt into the Gradle script:

  implementation & # 39; org.jetbrains.kotlinx: kotlinx-coroutines-core: 1.2.1 & # 39;
implementation & # 39; org.jetbrains.kotlinx: kotlinx-coroutines-android: 1.2.1 & # 39;

Sync the project to download the dependencies.

Lifecycle Awareness

Now that you have dependencies on Kotlin coroutines in your project, you can start implementing them. You start with the end in mind. It sounds kind of apocalyptic, but it's not! :]

You must prepare the code to clean up active coroutines before you begin implementing them. You will provide a way to cancel all active coroutines if the user decides to rotate or background the app, triggering the Fragment and Activity lifecycle.

You are going to extend these Android Life Cycle events to Kotlin classes that will handle coroutines internally.

Updating the repository

First, open Repository.kt and expand it LifecycleObserver . Then add a new feature that will allow you to disconnect the PhotosFragment life cycle. Add the feature as below:

  interface Repository: LifecycleObserver {
fun getPhotos (): LiveData <List >
fun getBanner (): LiveData 

funny register Lifecycle (lifecycle: Lifecycle)
}

Updating Injection Singleton

Next, open Injection.kt . Change the method signature on giveViewModelFactory () to include a Lifecycle parameter. Then, register Lifecycle in Repository .


 package com.raywenderlich.android.rwdc2018.app

import android.arch.lifecycle.Lifecycle
import com.raywenderlich.android.rwdc2018.repository.PhotosRepository
import com.raywenderlich.android.rwdc2018.repository.Repository
import com.raywenderlich.android.rwdc2018.ui.photos.PhotosViewModelFactory


object injection {

private fun supplyRepository (): Repository {
return PhotosPository ()
}

fun giveViewModelFactory (lifecycle: lifecycle): PhotosViewModelFactory {
val repository = supplyRepository ()
repository.registerLifecycle
return PhotosViewModelFactory (repository)
}
}

Updating PhotosFragment

Now, open PhotosFragment.kt . Give lifecycle as an argument in giveViewModelFactory () in at Attach .


 override fun at Attach (context: Context?) {
super.onAttach (context)

val viewModelFactory = Injection.provideViewModelFactory (lifecycle)
viewModel = ViewModelProviders.of (this, viewModelFactory) .get (PhotosViewModel :: class.java)
}

Register life cycle

Next, open PhotosRepository.kt again and implement the new feature.

  override parent directory Lifecycle (lifecycle: lifecycle) {
lifecycle.addObserver (this)
}

Main-Safe Design

Google encourages main security when writing coroutines. The concept is similar to how the Android system creates a main thread when an app is launched.

main thread is responsible for sending events to the appropriate UI widgets. You should delegate I / O and CPU intensive operations to a background thread to avoid in-app jams.

Main-safety is a design pattern for Kotlin coroutines. It allows coroutines to use Dispatchers.Main, or the main thread, as a standard thread context. They then favor delegation to Dispatchers.IO for heavy I / O operations or Dispatchers.Default for CPU heavy operations.

A CoroutineScope interface is available for classes that require scoped coroutines. However, you must define a CoroutineContext instance that the scope will use for all coroutines. Let's do it.

Defining CoroutineScope

First open PhotosRepository . Then implement CoroutineScope . Then define a Job and CoroutineScope .

  class PhotosRepository: Repository, CoroutineScope {
private val TAG = PhotosRepository :: class.java.simpleName
private selection job: Job = Job ()
override val coroutineContext: CoroutineContext
get () = Dispatchers.Main + job

//.. entered code ...

}

The job will determine if the coroutine is active, and you will then use to cancel it. According to the most important safe design pattern, defines Dispatchers.Main CoroutineScope .

Lifecycle Connection

Now add the following code to ] PhotosRepository . This interrupts all active coroutines when Android calls PhotosFragment Lifecycle.Event.ON_STOP .

  @OnLifecycleEvent (Lifecycle.Event.ON_STOP)
private Fun Cancel Job () {
Log.d (TAG, "CancelJob ()")
if (job.isActive) {
Log.d (TAG, "Job Active, Canceling")
job.cancel ()
}
}

A typical implementation is to include a Job instance plus a Dispatcher as context for the scope. The implementation of the interface allows you to call launch () anywhere and handle cancellation with the job you specified. The suspension functions can then call withContext (Dispatchers.IO) or withContext (Dispatchers.Default) to delegate work to background threads if necessary. Holde den første tråden bundet til hoved tråden.

Introducing Coroutines

Både fetchBanner () og fetchPhotos () bruker en Kan kjøres og henrettes med en ny tråd . Først må du endre metoden implementering for å bruke Kotlin coroutines. Deretter vil du kjøre prosjektet for å se om alt fungerer som før.

Banneret og bildene lastes ned i bakgrunnen. They’ll display like before with the separate background thread implementation.

// Dispatchers.Main
private suspend fun fetchBanner() {
  val banner = withContext(Dispatchers.IO) {
    // Dispatchers.IO
    val photosString = PhotosUtils.photoJsonString()
    // Dispatchers.IO
    PhotosUtils.bannerFromJsonString(photosString ?: "")
}
 // Dispatchers.Main
 if (banner != null) {
    // Dispatchers.Main
    bannerLiveData.value = banner
}
}

// Dispatchers.Main
private suspend fun fetchPhotos() {
  val photos = withContext(Dispatchers.IO) {
    // Dispatchers.IO
    val photosString = PhotosUtils.photoJsonString()
    // Dispatchers.IO
    PhotosUtils.photoUrlsFromJsonString(photosString ?: "")
}
  // Dispatchers.Main
  if (photos != null) {
    // Dispatchers.Main
    photosLiveData.value = photos
}
}

The functions above are annotated with comments. They show what thread or thread pool executes each line of code. Because they are now marked with suspendyou have to change these function declarations, to avoid compiler errors:

override fun getPhotos(): LiveData<List> {
  launch { fetchPhotos() }
  return photosLiveData
}

override fun getBanner(): LiveData {
  launch { fetchBanner() }
  return bannerLiveData
}

In this case, the thread is Dispatchers.Main and the thread pool is Dispatchers.IO. This helps visualize the main-safety design.

Now build and deploy the app to the emulator. Filter logcat with PhotosFragment and then background the app. In the log prints you’ll see a PhotsFragment Lifecycle.Event.ON_STOP triggering an active coroutine Job to cancel.

2019-05-21 22:35:06.937 29522-29522/com.raywenderlich.android.rwdc2018 D/PhotosRepository: cancelJob()
2019-05-21 22:35:06.937 29522-29522/com.raywenderlich.android.rwdc2018 D/PhotosRepository: Job active, canceling

Congratulations! You’ve successfully converted asynchronous code to Kotlin coroutines. And everything still works, but looks nicer! :]

Where to Go From Here?

You can download the completed project by clicking on the Download Materials button at the top or bottom of the tutorial. Continue building your understanding of Kotlin Coroutines and how you can use them in Android app development. Resources like Kotlin Coroutines by Tutorials, kotlinx.coroutines and Coroutines Guide are great references. If you have any questions or comments, please join the forum below.


Source link