Home Android & Kotlin Books Kotlin Coroutines by Tutorials

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.

Coroutines on Android: Part 3
Coroutines on Android: Part 3

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:

Starter App
Starter App

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:

Fetch from Local and Remote database
Fetch from Local and Remote database

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:

  1. Extensions.kt: This class includes custom Kotlin Extension methods to be used in the app.
  2. 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.

// Log Coroutines
fun logCoroutineInfo(msg: String) = println("Running on: [${Thread.currentThread().name}] | $msg")
logCoroutineInfo("Fetching from remote")
logCoroutineInfo("launch executed")
logCoroutineInfo("Fetching from local")
logCoroutineInfo("Got items from local")
logCoroutineInfo("Got items from remote")
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
System.setProperty("kotlinx.coroutines.debug", if (BuildConfig.DEBUG) "on" else "off")
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
withContext(CoroutineName("CustomName")) {
    // body
}
withContext(Dispatchers.IO + CoroutineName("CustomName")) {
    // body
}
var itemList = withContext(Dispatchers.IO + CoroutineName("Coroutine for Local")) {
    logCoroutineInfo("Fetching from local")
    repository?.getDataFromLocal()
}
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

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.

throw RuntimeException("My Runtime Exception: The Darkforce is strong with this one")
try{
    // Method body
}catch (e: Exception) {
    handleError(e)
}
override fun handleError(e: Exception) {
    // Hide loading animation
    view?.hideLoading()

    // prompt in view
    view?.prompt(e.message)
}
Handling My Runtime Exception using try-catch
Cenrzept Zm Parfesa Arweqrium etulv nns-lekgj

// Setup handler for uncaught exceptions.
Thread.setDefaultUncaughtExceptionHandler { _, e -> 
    Log.e("UncaughtExpHandler", e.message) 
}
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
private val handler = CoroutineExceptionHandler { _, throwable ->
    handleError(Exception(throwable.message))
}
launch(Dispatchers.IO + handler) {
    //body
}
launch(Dispatchers.IO + handler){
    ...
}
launch(Dispatchers.IO + handler){
    throw RuntimeException("My Runtime Exception: The Darkforce is strong with this one")
    ...
}
Handling My Runtime Exception using CoroutineExceptionHandler
Xedhhexy Lv Gelciza Udmejdoet aniqy SuniedupoExkucbuurPowgrum

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.

class MainActivityPresenter(var view: ViewContract?, 
    var repository: DataRepositoryContract?,
    uiDispatcher: CoroutineDispatcher = Dispatchers.Main,
    val ioDispatcher: CoroutineDispatcher = Dispatchers.IO)
// Setup the presenter
val presenter = MainActivityPresenter(this, repository)
testImplementation "io.mockk:mockk:1.9"
// 1
val repository: DataRepositoryContract = mockk(relaxUnitFun = true)

// 2
every { repository.getDataFromLocal() } returns mockedItemList
val mockData = MockData()
val mockedItemList= mockData.generateFakeData()
 @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()
  }
// When
// 1
presenter.updateData(mockedItemList, "Repo")


// Then
// 2
verify {
    view.prompt(any())
}

// 3
coVerify {
    repository.saveData(mockedItemList)
}

// 4
verifyOrder {
    view.hideLoading()
    view.updateWithData(mockedItemList)
}
// saveDataUsingCoroutines(it)
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)
// 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)
}

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 Coroutines
implementation "org.jetbrains.anko:anko-coroutines:0.10.8"
  // 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()
    }
}
Fetch from Remote server
Lehdr sxog Detucu zovbun

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.