18.
Coroutines on Android - Part 2
Written by Nishant Srivastava
Developing Android apps can get complex when dealing with asynchronous tasks. It is not immediately clear on what thread the code is executing, and trying to figure it out usually involves adding logging statements and debugging the code flow. More importantly, it is the best practice to be able to test the business logic of complex asynchronous tasks. It gets fairly more complex when coroutines are added to the system.
As you know by now, there could be hundreds of coroutines, and they can be executed easily because they are very lightweight. Now, imagine trying to debug a codebase wherein hundreds of these coroutines are executing. Thankfully, there are ways to handle those and the tooling/libraries are built to address such situations.
All of this and more will be covered in this chapter while working with an Android app. Without much ado, jump over to the next section in this chapter.
Getting started
For this chapter, you will start from where you left off in the last chapter, with the Android app called StarSync. As you already know, the app is an offline first MVP app. There is a repository that takes care of fetching data from the SWAPI API, which is a public Star Wars API. You can access the documentation for the same at https://swapi.co/. Once fetched, the data is saved to the local database using the Room architecture components library.
If you have already downloaded the starter project, then import it into Android Studio.
Run the starter app now and you will see the following:
Because this is an offline-first Android app, when the app loads for the first time, it tries to load data from the local database first. It then goes on to fetch from the remote server via a GET call to the SWAPI API:
After the first remote fetch, data is saved to the local database. To verify the offline-first approach, simply switch to Airplane Mode and re-launch the app. Data will be fetched from the local database and populated in the list on the screen.
Some important classes to look at:
-
Extensions.kt: This class includes custom Kotlin Extension methods to be used in the app. -
StarSyncApp.kt: This class extends the Application class and is the main entry point of the Android app. This class is used to set up configurations for the app when it loads up.
You will pick up where you left off from the last chapter — i.e., the app was set up to use coroutines to be able to function as intended. Next, you will enable logging capabilities in the app for coroutines.
Debugging coroutines
When you run the app, the various processes of fetching data from the local and remote repository’s are fired up using multiple coroutines. At the same time, there is context switching from executing a fetch operation on the background and then switching to the main thread to display the result when it is available.
While developing your app, you often need to know the name of the coroutine in which your code is executed, mostly in order to debug the app. Having that functionality makes visualizing the coroutines while they are executing much easier to follow. There is a utility method logCoroutineInfo already setup in Extensions.kt file:
// Log Coroutines
fun logCoroutineInfo(msg: String) = println("Running on: [${Thread.currentThread().name}] | $msg")
All it does is simply print to the standard output with a formatted text that includes the name of the thread. You can use this right away in the starter project.
Go to RemoteRepo.kt first, under package repository/remote. Now, find the method getDataUsingCoroutines() and replace the line with comment // TODO: Add a log with msg ’Fetching from remote’ with line of code as below:
logCoroutineInfo("Fetching from remote")
Next, go to MainActivityPresenter.kt under ui/mainscreen package and find the method fetchUsingCoroutines(). You will now add a couple of log statements by replacing TODO comments as below
Replace // TODO: Add a log with msg ’launch executed’ with:
logCoroutineInfo("launch executed")
Replace // TODO: Add a log with msg ’Fetching from local’ with:
logCoroutineInfo("Fetching from local")
Replace // TODO: Add a log with msg ’Got items from local’ with:
logCoroutineInfo("Got items from local")
Replace // TODO: Add a log with msg ’Got items from remote’ with:
logCoroutineInfo("Got items from remote")
Once done, run the app. Now open Logcat and filter for “Running on.” You will see the below output:
I/System.out: Running on: [main] | launch executed
I/System.out: Running on: [DefaultDispatcher-worker-1] | Fetching from local
I/System.out: Running on: [main] | Got items from local
I/System.out: Running on: [DefaultDispatcher-worker-2] | Fetching from remote
I/System.out: Running on: [main] | Got items from remote
Nice — now you have logs in order. But wait: If you take a closer look at the logs printed on the screen, the string between [] contains the thread name such as DefaultDispatcher-worker-2, and that is not very helpful. When using coroutines, the thread name alone does not give much of a context. It would be nice to be able to log the coroutines themselves as they are executing instead of the thread they are running on.
For the same reason, kotlinx.coroutines includes debugging facilities, but it needs to be enabled upfront by setting the -Dkotlinx.coroutines.debug to on as a JVM property.
In an Android app, you would typically do this inside the custom Application class. In the starter app, that would be inside the StarSyncApp.kt file.
Replace the comment // TODO: Enable Debugging for Kotlin Coroutines with:
System.setProperty("kotlinx.coroutines.debug", if (BuildConfig.DEBUG) "on" else "off")
That is it. Now, run the app, again. The output now changes to:
I/System.out: Running on: [main @coroutine#1] | launch executed
I/System.out: Running on: [DefaultDispatcher-worker-1 @coroutine#1] | Fetching from local
I/System.out: Running on: [main @coroutine#1] | Got items from local
I/System.out: Running on: [DefaultDispatcher-worker-3 @coroutine#1] | Fetching from remote
I/System.out: Running on: [main @coroutine#1] | Got items from remote
You will notice that the log statements now include a section pertaining to the coroutine with a number appended to it.
In debug mode, every coroutine is assigned a unique consecutive identifier. Every thread that executes a coroutine has its name modified to include the name and identifier of the currently running coroutine. When one coroutine is suspended and resumes another coroutine is dispatched in the same thread, then the thread name displays the whole stack of coroutine descriptions that are being executed on this thread.
However, this can be improved even more. When coroutines are tied to the processing of the specific request or doing some background-specific task, it is better to name it explicitly for debugging purposes. Automatically assigned IDs are usually good when you want to log coroutines often, and you just need to correlate log records coming from the same coroutine — but having a named coroutine just makes logs easy to consume and more focused.
To facilitate that functionality, kotlinx.coroutines provides a class called CoroutineName. This class allows you to specify a name for the coroutine and is typically passed to the coroutine as a context:
withContext(CoroutineName("CustomName")) {
// body
}
In this case, a coroutine already has a Dispatcher being passed as a context, one can add CoroutineName to the existing Dispatcher using the + operator like below:
withContext(Dispatchers.IO + CoroutineName("CustomName")) {
// body
}
You will make the same change to provide a name to the coroutine used for fetching from the local database. In the MainActivityPresenter.kt, under ui/mainscreen package, change it to the following:
var itemList = withContext(Dispatchers.IO + CoroutineName("Coroutine for Local")) {
logCoroutineInfo("Fetching from local")
repository?.getDataFromLocal()
}
Now, run the app and open the Logcat to track the logs. You will be able to see something like below:
I/System.out: Running on: [main @coroutine#1] | launch executed
I/System.out: Running on: [DefaultDispatcher-worker-1 @Coroutine for Local#1] | Fetching from local
I/System.out: Running on: [main @coroutine#1] | Got items from local
I/System.out: Running on: [DefaultDispatcher-worker-3 @coroutine#1] | Fetching from remote
I/System.out: Running on: [main @coroutine#1] | Got items from remote
Notice the second log statement, which now contains the name for the coroutine as @Coroutine for Local#1. This looks much better.
Note: CoroutineName context element is displayed in the thread name that is executing this coroutine only when debugging mode is turned on.
Exception handling
Exception handling in coroutines was covered in previous chapters extensively, thus our focus here will be on their behavior on the Android platform.
Before you look into how exceptions are handled by coroutines on Android, understand what actually happens when an exception is thrown in a coroutine:
-
The exception is caught and then resumed through a Continuation.
-
If your code doesn’t handle the exception, and it isn’t a CancellationException, the first CoroutineExceptionHandler is requested through the current CoroutineContext.
-
If a handler isn’t found or it errors, the exception is sent to platform-specific code.
-
On the JVM, a ServiceLoader is used to locate global handlers.
-
Once all handlers have been invoked, or one of them has errors, the current thread’s exception handler gets invoked.
-
If the current thread doesn’t handle the exception, it bubbles up to the thread group and then finally to the default exception handler.
-
Crash!
Android, as a platform, has many ways by which an exception can be handled. The most common one being try-catch. Coroutines use the same to handle exceptions. To see how it looks like in practice, navigate to MainActivityPresenter.kt under ui/mainscreen package.
Inside fetchUsingCoroutines() method, a try-catch is already setup around the body of the method. You will need to trigger a RuntimeException to force an exception.
Replace // TODO: Force a Runtime crash here (for demonstrating try catch behavior) with:
throw RuntimeException("My Runtime Exception: The Darkforce is strong with this one")
Notice that the catch inside the fetchUsingCoroutines() method calls handleError(e: Exception) method:
try{
// Method body
}catch (e: Exception) {
handleError(e)
}
Where handleError(e: Exception) is defined as inside the MainActivityPresenter.kt file itself:
override fun handleError(e: Exception) {
// Hide loading animation
view?.hideLoading()
// prompt in view
view?.prompt(e.message)
}
This means that, when the RuntimeException is thrown, you should see a prompt on the screen and the loading state will be hidden. Now, run the app.
You will see a snackbar show up with the RuntimeException message you set earlier: “My Runtime Exception: The Darkforce is strong with this one.”
In the case of exceptions that are not handled on Android, there exists an UncaughtExceptionHandler, which can be configured in the Application class. By default, coroutines use the default Android policy on uncaught exception handling if no try-catch is set up for exception handling.
To set up your own UncaughtExceptionHandler, you will need to define a new UncaughtExceptionHandler and set it as the default UncaughtExceptionHandler, as shown below:
// Setup handler for uncaught exceptions.
Thread.setDefaultUncaughtExceptionHandler { _, e ->
Log.e("UncaughtExpHandler", e.message)
}
Note: This is already defined and set up in the StarSyncApp.kt file in the starter app.
Now, to see this functioning in practice, you will need to remove the try-catch inside the fetchUsingCoroutines() method and run the app. This time, the app will start and will be stuck in the loading state. Open the Logcat window inside Android Studio. You will notice a stacktrace of a RuntimeException, as well as the log statement you added to your UncaughtExceptionHandler:
com.raywenderlich.android.starsync E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.raywenderlich.android.starsync, PID: 12774
java.lang.RuntimeException: My Runtime Exception: The Darkforce is strong with this one
at com.raywenderlich.android.starsync.ui.mainscreen.MainActivityPresenter$fetchUsingCoroutines$1.invokeSuspend(MainActivityPresenter.kt:67)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
com.raywenderlich.android.starsync E/UncaughtExpHandler: My Runtime Exception: The Darkforce is strong with this one
Why didn’t the app crash, though? Although you removed the try-catch earlier, you also setup a default UncaughtExceptionHandler, which consumes all uncaught exceptions and logs them to the Logcat as per the definition you set up.
Another way to handle exceptions is to create a CoroutineExceptionHandler that logs the exception directly. A CoroutineExceptionHandler is already defined inside the MainActivityPresenter.kt under ui/mainscreen package named as handler:
private val handler = CoroutineExceptionHandler { _, throwable ->
handleError(Exception(throwable.message))
}
To use the handler with a coroutine, simply pass it along with the context using the + operator as shown below:
launch(Dispatchers.IO + handler) {
//body
}
You can set it up in the starter app right away. Do as follows: Go to // TODO: Setup CoroutineExceptionHandler for launch coroutine here and update the launch coroutine to:
launch(Dispatchers.IO + handler){
...
}
Next, replace // TODO: Force a Runtime crash here (for demonstrating CoroutineExceptionHandler behavior) and with a RuntimeException as shown below:
launch(Dispatchers.IO + handler){
throw RuntimeException("My Runtime Exception: The Darkforce is strong with this one")
...
}
Run the app, now. You will see the app show a snackbar with the error message you defined in the RuntimeException:
When a launch coroutine crashes, its parent is canceled, which in turn cancels all the parent’s children. Once coroutines throughout the tree have finished canceling, the exception is sent to the current context’s exception handler. On Android, that means your app will crash, regardless of what dispatcher you were using if the exception was not handled.
On the other hand, async holds on to its exceptions. What that means is that await() explicitly handles all exceptions and installing a CoroutineExceptionHandler will have no effect.
All these exceptions and errors need to be handled, but if you wrote tests for your app you can always be sure these situations are not unexpectedly rising up.
Don’t forget testing
Tests in an Android app are one of the most important aspects of building a quality app. Having tests in place makes sure that the business logic is correct and the app executes as expected. When it comes to asynchronous programming, it becomes even more important to have tests in place. Because of complex timing and execution states, the multi-threaded nature of async operations on the Android platform increases the chances of errors.
One can spawn multiple Kotlin coroutines on a background thread or the main thread. But when it comes to testing, you want to set a specific thread on which your tests run, thereby removing ambiguity around where the coroutines are executing and eliminating context switching completely. To enable that kind of functionality, you will need to make sure you can specify the Dispatchers your coroutines run on.
In the starter app, you will notice all the coroutines are executing inside the MainActivityPresenter class with each having their own Dispatcher defined to execute on. You need to now modify the MainActivityPresenter class under ui/mainscreen package so that its constructor can take in two more arguments as below:
class MainActivityPresenter(var view: ViewContract?,
var repository: DataRepositoryContract?,
uiDispatcher: CoroutineDispatcher = Dispatchers.Main,
val ioDispatcher: CoroutineDispatcher = Dispatchers.IO)
Notice the two new arguments now being passed to the primary constructor of the MainActivityPresenter class. To make sure the API does not break, default values are provided.
This means if you do not pass in arguments, the default values will be passed in — i.e., inside the MainActivity.kt file, the call to initialize MainActivityPresenter does not need to be changed:
// Setup the presenter
val presenter = MainActivityPresenter(this, repository)
The above initialization of presenter is completely valid, and, since no third or fourth arguments are provided, the default ones will be used to initialize the presenter.
Navigate back to the MainActivityPresenter.kt file under ui/mainscreen package and replace all occurrences of Dispatchers.Main with uiDispatcher and Dispatchers.IO with ioDispatcher.
This makes sure that when you do pass in values to those arguments, the set Dispatchers will be used for executing the coroutines. Now, the MainActivityPresenter class is testable.
However, the constructor for MainActivityPresenter class also accepts arguments such as instances of ViewContract and DataRepositoryContract. These are dependencies that need to be defined when writing tests.
That is a problem because these can change and writing tests with dependencies that can change makes the tests flaky — i.e., not reliable. To fix this issue, these dependencies should be mocked out.
Note: Mocked objects are simulated objects that mimic the behavior of real objects in controlled ways. Mocking for tests is vast subject and is out of the bounds of this book.
There is a helper MockData class under the test/<app_packagename>/repository that provides a simple method called generateFakeData(), which simply returns a list of fake People objects. This list will be used to validate against in our tests. Typically, this a mocked out version of a list of People objects.
You are going to use one of the mocking libraries for Kotlin called Mockk. This library is written specifically for Kotlin and enables mocking final classes, which are the default in Kotlin as well as providing many helper DSLs (Domain Specific Language) to write tests in a more idiomatic manner.
To add it to the starter project, navigate to your build.gradle file for the app module and replace // TODO: add Mockk dependency here with below:
testImplementation "io.mockk:mockk:1.9"
Note: To be able to mock and verify coroutines, you need to have the
kotlinx-coroutines-coredependency, which, in our case, is already added in the build.gradle for the app module.
Now, sync your project. Once your project is synced and the Mockk dependency is available during runtime, you will have access to various methods such as mockk, verify, every, verifyOrder, etc. methods, which allow mocking Kotlin classes and coroutines behavior.
As a short primer about using Mockk, you need to understand the flow of how the tests are written. You will most likely mock out the classes you want to define a set behavior for when running under your tests. Then, you will need to define what happens on every call to a certain method on those classes. This is where the every{} DSL from Mockk comes into play. It basically allows you to define what to return immediately when a call to a certain method on a mocked object is made — for example:
// 1
val repository: DataRepositoryContract = mockk(relaxUnitFun = true)
// 2
every { repository.getDataFromLocal() } returns mockedItemList
Here:
- Initializes the
repositoryvariable, which is of typeDataRepositoryContractclass with a mocked version ofDataRepositoryContract. The partrelaxUnitFun = truepassed to themockk()method only means that Mockk library will not throw an error when trying to Mockk methods that return nothing, i.e., Unit. It relaxes the strict behavior. - Defines on every call of
getDataFromLocal()method on the mockedrepositoryinstance return a list of mocked out list items. WheremockedItemListis an ArrayList of mocked out list items defined as below:
val mockData = MockData()
val mockedItemList= mockData.generateFakeData()
Using this knowledge, you will now update the Unit Test file created against the MainActivityPresenter class called MainActivityPresenterTest. Navigate to MainActivityPresenterTest.kt file under tests package.
Next, implement the setUp and tearDown methods replacing // TODO comments as below:
@Before
fun setUp() {
// 1
repository = mockk(relaxUnitFun = true)
//2
view = mockk(relaxUnitFun = true)
// 3
presenter = MainActivityPresenter(view, repository, Dispatchers.Unconfined, Dispatchers.Unconfined)
}
@After
fun tearDown() {
// 4
unmockkAll()
}
Here:
- Mock the repository.
- Mock the view.
- Initialize the presenter, by passing in the mocked-out view and repository as arguments to the constructor. For the uiDispatcher and ioDispatcher, you pass in
Dispatchers.Unconfined. It is the dispatcher that is not confined to any specific thread; i.e., it runs on the same thread as the one on which the coroutine was launched. - Finally, when the tests all finish executing, simply reset the mocks so as to create a clean slate for the next time the tests are run.
Now that the setup is done, you only need to write the tests to validate the business logic for the MainActivityPresenter class.
The first test you will write is to validate the logic for updateData() method inside the presenter.
Inside the MainActivityPresenterTest.kt file under tests package, a test stub already exists called presenter_updateData(). Simply replace the // TODO: Add presenter_updateData() implementation here with:
// When
// 1
presenter.updateData(mockedItemList, "Repo")
// Then
// 2
verify {
view.prompt(any())
}
// 3
coVerify {
repository.saveData(mockedItemList)
}
// 4
verifyOrder {
view.hideLoading()
view.updateWithData(mockedItemList)
}
Going through the code step by step:
- At the start of the test, call the
presenter.updateData(mockedItemList, "Repo"). - Then,
verifythat theview.prompt()method withany()argument passed to it is called. - Then
coVerifychecks if the coroutinerepository.saveData(mockedItemList)was called. - Next
verifythatview.hideLoading()andview.updateWithData(mockedItemList)are called one after the other in order.
If all of these hold true, then the test would pass. This is the happy path that you just implemented, and the test will pass in our case because the starter app is written with the correct business logic. Try running the test by right-clicking on the test and selecting Run ’presenter_updateData()’.
To validate if the tests are functional, try to change the business logic in the app. Navigate to MainActivityPresenter.kt file under ui/mainscreen and comment the saveDataUsingCoroutines(it) inside the updateData() method definition:
// saveDataUsingCoroutines(it)
Now, go back to MainActivityPresenterTest.kt file under the tests package and execute the test called presenter_updateData() once again. This time the test will fail denoting that the business logic is not correct and needs to be fixed. You will be looking at a stacktrace:
ava.lang.AssertionError: Verification failed: call 1 of 1: DataRepositoryContract(#1).saveData(eq([People(name=Luke Skywalker, height=172, mass=77, hair_color=blond, skin_color=fair, eye_color=blue, gender=male), People(name=Darth Vader, height=202, mass=136, hair_color=none, skin_color=white, eye_color=yellow, gender=male), People(name=Leia Organa, height=150, mass=49, hair_color=brown, skin_color=light, eye_color=brown, gender=female)]))) was not called
at io.mockk.impl.recording.states.VerifyingState.failIfNotPassed(VerifyingState.kt:66)
at io.mockk.impl.recording.states.VerifyingState.recordingDone(VerifyingState.kt:42)
at io.mockk.impl.recording.CommonCallRecorder.done(CommonCallRecorder.kt:48)
at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:60)
at io.mockk.impl.eval.VerifyBlockEvaluator.verify(VerifyBlockEvaluator.kt:27)
at io.mockk.MockKDsl.internalCoVerify(API.kt:143)
at io.mockk.MockKKt.coVerify(MockK.kt:162)
at io.mockk.MockKKt.coVerify$default(MockK.kt:159)
at com.raywenderlich.android.starsync.ui.mainscreen.MainActivityPresenterTest.presenter_updateData(MainActivityPresenterTest.kt:99)
Uncomment the saveDataUsingCoroutines(it) inside the updateData() method definition in MainActivityPresenter.kt file to get back to a functional business logic and re-run the test to validate it.
The second test you will write is to validate the logic for the getData() method inside the presenter.
Inside the MainActivityPresenterTest.kt file under the tests package, a test stub already exists called presenter_getData(). Simply replace the // TODO: Add presenter_getData() implementation here with:
// Given
every { repository.getDataFromLocal() } returns mockedItemList
coEvery { repository.getDataFromRemoteUsingCoroutines()} returns mockedItemList
// When
presenter.getData()
// Then
verifyOrder {
view.hideLoading()
view.showLoading()
}
coVerify {
repository.getDataFromLocal()
}
verify {
view.prompt(any())
}
coVerify {
repository.saveData(mockedItemList)
}
verifyOrder {
view.hideLoading()
view.updateWithData(mockedItemList)
}
coVerify {
repository.getDataFromRemoteUsingCoroutines()
}
verify {
view.prompt(any())
}
coVerify {
repository.saveData(mockedItemList)
}
verifyOrder {
view.hideLoading()
view.updateWithData(mockedItemList)
}
This test is very similar to the one explained above. This is, again, the happy path and when you run this test, it will pass successfully. If, however, you change the business logic, the test will fail — thus, acting as a safeguard against unintended changes in future.
Anko: Simplified coroutines
Kotlin coroutines are essentially a language feature. Similar to how the standard kotlin.coroutines library builds upon them, Anko (ANdroid KOtlin) coroutines is another library that is based on the kotlin.coroutines library, providing simpler syntax and approach to async programing.
Anko is a helper library built by the folks at JetBrain. Anko was originally designed as a single library. As the project grew, adding Anko as a dependency began to have a significant impact on the size of the APK, hence it was split out into sub-libraries, namely:
-
Commons: Helps you perform the most common Android tasks, including displaying dialogues and launching new Activities.
-
Layouts: Provides a Domain Specific Language (DSL) for defining Android layouts.
-
SQLite: A query DSL and parser that makes it easier to interact with SQLite databases.
-
Coroutines: Supplies utilities based on the
kotlinx.coroutineslibrary. -
Anko-Coroutines, as of writing this chapter, provides access to only one helper method called
asReference().
By default, a coroutine holds references to captured objects until it is finished or cancelled. That means in the Android world, it will capture/hold on to the instance of Activity or Fragment, until it is finished or cancelled. If not cancelled/finished, this might lead to memory leaks.
Consider a common use case of using a coroutine as an asynchronous API. Once called, the API would execute the coroutine to do the specific work, suspend it and then resume it back. Pretty simple :]
However there is a caveat here. If the asynchronous API does not support cancellation, your coroutine may be suspended for an indefinite time period. In Android world, the coroutine would hold a reference to the instance of the Activity/Fragment indefinitely, which is pretty bad as it leads to memory leaks.
To avoid such a situation, the asReference() function creates a weak reference wrapper around the instance of a Activity/Fragment to protect against memory leaks.
To start using asReference(), open the starter app and navigate to the app/build.gradle file and replace // TODO: add Anko Coroutines dependency here with the below:
// Anko Coroutines
implementation "org.jetbrains.anko:anko-coroutines:0.10.8"
Sync your project. Now, the asReference() function should be available to you. You will now implement a new function that fetches data from the remote repo and updates the result in the UI, called fetchResultUsingAnkoCoroutine().
Navigate to MainActivityPresenter.kt file under the ui/mainscreen package and replace //TODO: Anko implementation with:
// Anko implementation
// 1
private lateinit var job: Job
private fun fetchResultUsingAnkoCoroutine() {
// 2
val ref = asReference()
// 3
job = launch(uiDispatcher) {
try {
// 4
val deferred = async(ioDispatcher) {
repository?.getDataFromRemoteUsingCoroutines()
}
// 5
ref().apply {
// Prompt in view about the source of data
view?.prompt("Loading data from Remote")
// Hide loading animation
view?.hideLoading()
// Update view
view?.updateWithData(deferred.await()?: emptyList())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
Here,
- An instance of a
Jobclass is declared withlateinitmodifier so as to initialize it later. - Inside the function, a reference is acquired of the current class; i.e., the presenter class using
asReference()function from the Anko-Coroutines library. This creates a weak reference around the presenter. - Assign the
jobinstance with the job returned from the launch() coroutine builder. - Execute the async coroutine builder to handle call to
repository?.getDataFromRemoteUsingCoroutines()function in background, returning a Deferred instance. - Using the
refinstance defined earlier, reference the view and update the UI.
Next, navigate to the function fetchData() and replace the call to fetchUsingCoroutines() with fetchResultUsingAnkoCoroutine(). Also call job.cancel() inside the cleanup() function, which makes sure the coroutines are canceled when cleanup()function is called. The ref instance makes sure that no strong references are kept around.
Now, run the app and you will notice that the data is fetched from the remote repository and displayed in the UI as a list, as shown on the next page.
Note: Anko-Coroutines has more helper methods such as
bg()anddoAsync(), which are now deprecated in favor of the standardkotlin.coroutineslibrary’s implementations.
Key points
Android is an ever-evolving platform, each year a new flavor of Android is released. The complexity with each new release around async processing also increases as new APIs are released. New devices with completely different setups are being released, such as foldable phones. Handling the Activity/Fragment lifecycles and managing the app states is going to become more complex. Thankfully, Kotlin coroutines are a step forward in simplification of async processes, enabling well testable apps.
- Debugging coroutines is pretty easy since you can name them and also log the name of the thread they are running on.
- To enable debugging logs in coroutines,
-Dkotlinx.coroutines.debugflag needs to be set as a JVM property. - By default, coroutines use the default Android policy on uncaught exception handling if no try-catch is set up for exception handling.
- Using
CoroutineExceptionHandler, you can set up a custom handler for exceptions generated from coroutines. -
Dispatchers.Unconfineddispatcher is not confined to any specific thread; i.e., it runs on the same thread as the one on which the coroutine was launched. - In order to make coroutines testable, the normal dispatchers need to be replaced by
Dispatchers.Unconfinedinside test methods. - Mockk is a Kotlin mocking library, which allows to mock coroutines as well and tests their execution points.
-
Anko (ANdroid KOtlin) is a set of helper libraries built by the folks at JetBrain. The Anko coroutines library is based on the standard
kotlin.coroutineslibrary.