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.
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.
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
.
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.
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.
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 suspend
you 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