Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

First Edition · Android 12, iOS 15, Desktop · Kotlin 1.6.10 · Android Studio Bumblebee

13. Concurrency
Written by Carlos Mota

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

As an app gets more complex, concurrency is a fundamental topic you’ll need to address. learn makes multiple requests to the network — that must be done asynchronously to guarantee they won’t impact the UI.

Note: This chapter follows the project you started in Chapter 12, “Networking”. Or, you can use this chapter’s starter project.

Here’s what you’ll do in this chapter:

  • You’ll learn what coroutines are and how you can implement them.
  • You’ll enable the new Kotlin/Native memory model.

The need for structured concurrency

Structured concurrency allows doing multiple computations outside the UI-thread to keep the app as responsive as possible. It differs from concurrency in the sense that a task can only run within the scope of its parent, which cannot end before all of its children.

UI-thread secondary thread task #1 #2 #3 segments structured concurrency screen refresh rate coroutine #1 coroutine #2 coroutine #3
Fig. 13.1 - Diagram showing multiple tasks running in structured concurrency.

To improve its performance, these three tasks are running in the same thread, but concurrently to each other. They were divided into smaller segments that run independently.

Structured concurrency recently gained a lot of popularity with the releases of kotlinx.coroutines for Android and async/await for iOS — mainly due to how easy it is now to run asynchronous operations.

Different concurrency solutions

There’s a set of libraries built for Kotlin Multiplatform that support concurrency:

Understanding kotlinx.coroutines

Ktor uses coroutines to make network requests without blocking the UI-thread, so you’ve already used them unwittingly in the previous chapter.

for (feed in content) {
  fetchFeed(feed.platform, feed.url)
}

Suspend functions

Suspend functions are at the core of coroutines. As the name suggests, they allow you to pause a coroutine and resume it later on, without blocking the main thread.

public suspend fun fetchRWEntry(feedUrl: String): HttpResponse = client.get(feedUrl)

public suspend fun fetchMyGravatar(hash: String): GravatarProfile =
  client.get("$GRAVATAR_URL$hash$GRAVATAR_RESPONSE_FORMAT") {
    header(X_APP_NAME, APP_NAME)
  }.body()
OU-ghwoow quk sepeetihu mozxrTaow() DaefXneda().raints { //xciiwu yexaahudi } sufjusl vus // lehukiucotakb // futaywiwg fri AU bitqigd lad qanvambk vaebohx kor vugfid vaphahqu xahosaq cotuaggihf BY loikr 6 9 4 1 3 1 guqqsVuey() yersyGief() JilgsNBAfnxb() ahhuqiWejmtVMIwjsy() akkazeMardbKSOwpdp()
Cop. 99.2 - Huatfev qbegevn vyu robpawikb ffunb ey o wuljewg wuxeokh togv coxeusanoc.

Coroutine scope and context

Return to FeedPresenter.kt from shared/commonMain/presentation and search for the fetchFeed function:

private fun fetchFeed(platform: PLATFORM, feedUrl: String, cb: FeedData) {
  MainScope().launch {
    // Call to invokeFetchRWEntry
  }
}
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
public fun fetchMyGravatar(cb: FeedData) {
  //1
  CoroutineScope(Dispatchers.Default).launch {
    //2
    val profile = feed.invokeGetMyGravatar(
      hash = md5(GRAVATAR_EMAIL)
    )

    //3
    withContext(Dispatchers.Main) {
      //4
      cb.onMyGravatarData(profile)
    }
  }
}
public suspend fun invokeGetMyGravatar(
  hash: String,
): GravatarEntry {
  return try {
    val result = FeedAPI.fetchMyGravatar(hash)
    Logger.d(TAG, "invokeGetMyGravatar | result=$result")

    if (result.entry.isEmpty()) {
      GravatarEntry()
    } else {
      result.entry[0]
    }
  } catch (e: Exception) {
    Logger.e(TAG, "Unable to fetch my gravatar. Error: $e")
    GravatarEntry()
  }
}

Coroutine builders, scope and context

You’ve seen how to start a coroutine by calling launch. This function is part of the coroutine builders:

private suspend fun fetchMyGravatar(): GravatarEntry {
  return CoroutineScope(Dispatchers.Default).async {
    feed.invokeGetMyGravatar(
      hash = md5(GRAVATAR_EMAIL)
    )
  }.await()
}
private suspend fun fetchMyGravatar(): GravatarEntry {
  return withContext(CoroutineScope(Dispatchers.Default).coroutineContext) {
    feed.invokeGetMyGravatar(
      hash = md5(GRAVATAR_EMAIL)
    )
  }
}
public fun fetchMyGravatar(cb: FeedData) {
  Logger.d(TAG, "fetchMyGravatar")

  CoroutineScope(Dispatchers.Default).launch {
    cb.onMyGravatarData(fetchMyGravatar())
  }
}
func fetchProfile() {
  FeedClient.shared.fetchProfile { profile in
    Logger().d(tag: TAG, message: "fetchProfile: \(profile)")
    DispatchQueue.main.async {
      self.profile = profile
    }
  }
}

Cancelling a coroutine

Although you’re not going to use it in learn, it’s worth mentioning that you can cancel a coroutine by calling cancel() on the Job object returned by launch.

val deferred = CoroutineScope(Dispatchers.Default).async {
  feed.invokeGetMyGravatar(
    hash = md5(GRAVATAR_EMAIL)
  )
}

//If you want to cancel
deferred.cancel()

//If you want to wait for the result
deferred.await() 

Structured concurrency in iOS

Apple has a similar solution for structured concurrency: async/await.

private func fetchMyGravatar() async -> GravatarEntry {
  return await feed.invokeGetMyGravatar(
      hash = md5(GRAVATAR_EMAIL)
  )
}
private suspend fun fetchMyGravatar(): GravatarEntry {
  return withContext(Dispatchers.IO) {
    feed.invokeGetMyGravatar(
      hash = md5(GRAVATAR_EMAIL)
    )
  }
}
private func fetchMyGravatar() {
  Task {
    let profile = await feed.invokeGetMyGravatar(
      hash = md5(GRAVATAR_EMAIL)
    )
    await profile
  }
}
private suspend fun fetchMyGravatar() = {
  CoroutineScope(Dispatchers.IO).launch {
    async { feed.invokeGetMyGravatar(
        hash = md5(GRAVATAR_EMAIL)
      )
    }.await
  }
}

Using kotlinx.coroutines

It’s time to update learn. In the previous chapter, you learned how to implement the networking layer in Multiplatform. For this, you added the Ktor library and wrote the logic to fetch the raywenderlich.com RSS feed and parse its responses that later update the UI.

Adding kotlinx.coroutines to your Gradle configuration

Since Ktor includes the kotlinx.coroutines, when you added this library to the project, you were in the background adding both libraries.

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt") {
  version {
    strictly("1.6.0-native-mt")
  }
}

Implementing Dispatchers: IO for iOS

Although without the new Kotlin/Native memory model, iOS is single-threaded, this doesn’t mean that you can’t take advantage of multithreading on the other platforms. However, you’ll need to implement this support.

internal expect val ioDispatcher: CoroutineContext
import kotlin.coroutines.CoroutineContext
internal actual val ioDispatcher: CoroutineContext
    get() = Dispatchers.IO
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
package com.raywenderlich.learn.domain

internal actual val ioDispatcher: CoroutineContext
    get() = IosMainDispatcher
public object IosMainDispatcher : CoroutineDispatcher() {

  override fun dispatch(context: CoroutineContext, block: Runnable) {
    dispatch_async(dispatch_get_main_queue()) { block.run() }
  }
}
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
private val scope = CoroutineScope(ioDispatcher)
Fig. 13.3 - Feed in Android App
Tat. 62.6 - Seil ur Akykiin Avl

Fig. 13.4 - Feed in iOS App
Vub. 45.0 - Koeq ay iIS Owr

Fig. 13.5 - Feed in Desktop App
Qah. 68.5 - Wiov ir Rozlzaj Idj

Troubleshooting kotlinx.coroutines in iOS

As you continue your journey with Multiplatform outside this book, you’ll probably find this error:

Frozen state

In some instances, you might need to freeze your objects when running your iOS app to avoid having the error mentioned above. Once freeze()is called over an object, it becomes immutable. In other words, it can never be changed — allowing it to be shared across different threads.

Working with kotlinx.coroutines

On the app, go to the latest screen. You’ll see a couple of articles grouped into the different sections that you can swipe and open, but none of them has an image. It’s time to change this!

Creating a suspend function

Start by opening the FeedAPI.kt file from data/commonMain in the shared module. After the fetchRWEntry, add:

public suspend fun fetchImageUrlFromLink(link: String): HttpResponse = client.get(link) {
  header(HttpHeaders.Accept, "text/html")
}
//1
public suspend fun invokeFetchImageUrlFromLink(
  link: String,
  //2
  onSuccess: (String) -> Unit,
  onFailure: (Exception) -> Unit
) {
  try {

    //3
    val result = FeedAPI.fetchImageUrlFromLink(link)
    //4
    val url = parsePage(result.bodyAsText())

    //5
    coroutineScope {
      onSuccess(url)
    }
  } catch (e: Exception) {
    coroutineScope {
      onFailure(e)
    }
  }
}

Creating a coroutine with launch

Now that you’ve implemented the functions for requesting and parsing data, you’re just missing creating a coroutine, and it’s… launch. :]

public fun fetchLinkImage(platform: PLATFORM, id: String, link: String, cb: FeedData) {
  scope.launch {
    feed.invokeFetchImageUrlFromLink(
      link,
      onSuccess = { cb.onNewImageUrlAvailable(id, it, platform, null) },
      onFailure = { cb.onNewImageUrlAvailable(id, "", platform, it) }
    )
  }
}
override fun onNewImageUrlAvailable(id: String, url: String, platform: PLATFORM, exception: Exception?) {
  viewModelScope.launch {
    Logger.d(TAG, "onNewImageUrlAvailable | platform=$platform | id=$id | url=$url")
    val item = _items[platform]?.firstOrNull { it.id == id } ?: return@launch
    val list = _items[platform]?.toMutableList() ?: return@launch
    val index = list.indexOf(item)

    list[index] = item.copy(imageUrl = url)
    _items[platform] = list
  }
}
_items[platform] = if (items.size > FETCH_N_IMAGES) {
  items.subList(0, FETCH_N_IMAGES)
} else{
  items
}

for (item in _items[platform]!!) {
  fetchLinkImage(platform, item.id, item.link)
}
private fun fetchLinkImage(platform: PLATFORM, id: String, link: String) {
  Logger.d(TAG, "fetchLinkImage | link=$link")
  presenter.fetchLinkImage(platform, id, link, this)
}
public func fetchLinkImage(_ platform: PLATFORM, _ id: String, _ link: String, completion: @escaping FeedHandlerImage) {
  feedPresenter.fetchLinkImage(platform: platform, id: id, link: link, cb: self)
  handlerImage = completion
}
Fig. 13.6 - Android App: Browse Through the Latest Articles
Qov. 56.0 - Ufvsaag Idy: Gwamxo Fmguohj tbi Hubelx Ujlabfod

Fig. 13.7 - Desktop App: Browse Through the Latest Articles
Car. 30.7 - Vufcbar Okx: Dtoydi Dbjienc chu Batibq Avcukcuf

Fig. 13.8 - iOS App: Browse Through the Latest Articles
Keb. 73.5 - uEB Arl: Wxacta Rrkiuxd xli Moxasw Aldaykon

Creating a Coroutine with async

Alternatively to the previous approach where you’re using callbacks to notify the UI when new data is available, you can suspend the fetchLinkImage function until there’s a final result. For that, you’ll need to use async instead of launch.

public suspend fun fetchLinkImage(link: String): String {
  return scope.async {
    feed.invokeFetchImageUrlFromLink(
      link
    )
  }.await()
}
public suspend fun fetchLinkImage(link: String): String {
  return withContext(scope.coroutineContext) {
    feed.invokeFetchImageUrlFromLink(
      link
    )
  }
}
public suspend fun invokeFetchImageUrlFromLink(
    link: String
): String {
  return try {

    val result = FeedAPI.fetchImageUrlFromLink(link)
    parsePage(result.bodyAsText())

  } catch (e: Exception) {
    ""
  }
}
private fun fetchLinkImage(platform: PLATFORM, id: String, link: String) {
  Logger.d(TAG, "fetchLinkImage | link=$link")
  viewModelScope.launch {
    val url = presenter.fetchLinkImage(link)

    val item = _items[platform]?.firstOrNull { it.id == id } ?: return@launch
    val list = _items[platform]?.toMutableList() ?: return@launch
    val index = list.indexOf(item)

    list[index] = item.copy(imageUrl = url)
    _items[platform] = list
  }
}
public typealias FeedHandlerImage = (_ url: String) -> Void
@MainActor
public func fetchLinkImage(_ link: String, completion: @escaping FeedHandlerImage) {
  Task {
    do {
      let result = try await feedPresenter.fetchLinkImage(link: link)
      completion(result)
    } catch {
      Logger().e(tag: TAG, message: "Unable to fetch article image link")
    }
  }
}
@MainActor
func fetchFeedsWithPreview() {
  for platform in self.items.keys {
    guard let items = self.items[platform] else { continue }
    let subsetItems = Array(items[0 ..< Swift.min(self.fetchNImages, items.count)])
    for item in subsetItems {
      FeedClient.shared.fetchLinkImage(item.link) { url in
        guard var list = self.items[platform.description] else {
          return
        }
        guard let index = list.firstIndex(of: item) else {
          return
        }

        list[index] = item.doCopy(
          id: item.id,
          link: item.link,
          title: item.title,
          summary: item.summary,
          updated: item.updated,
          imageUrl: url,
          platform: item.platform,
          bookmarked: item.bookmarked
        )

        Logger().d(tag: TAG, message: "\(list[index].title)Updated to:\(list[index].imageUrl)")

        self.items[platform.description] = list
      }
    }
  }
}
Fig. 13.9 - Android App: Browse Through the Latest Articles
Cak. 23.1 - Igmqeil Uvd: Mwiqso Kjhousw nvi Serexj Ovrupkup

Fig. 13.10 - Desktop App: Browse Through the Latest Articles
Mos. 81.35 - Geqwget Ofh: Nsosga Vcgiilb rzo Vuvorq Iyruggat

Fig. 13.11 - iOS App: Browse Through the Latest Articles
Viw. 63.43 - uEH Iqf: Pjewta Zcgautd pdi Piqatj Ercomqup

New Kotlin/Native memory model

Throughout this book, you’ve seen a couple of scenarios where you needed to create a specific implementation for iOS:

Enabling the new Kotlin/Native memory model

Learn is already using the latest libraries compatible with the new Kotlin/Native memory model:

#Enable Kotlin/Native Memory Model
kotlin.native.binary.memoryModel=experimental
kotlin.native.binary.freezing=disabled
get() = Dispatchers.Default
func fetchFeeds() {
  FeedClient.shared.fetchFeeds { platform, items in
    Logger().d(tag: TAG, message: "fetchFeeds: \(items.count) items | platform: \(platform)")
    DispatchQueue.main.async {
      self.items[platform] = items
    }
  }
}
Fig. 13.12 - iOS App: Browse Through the Latest Articles
Nor. 28.85 - eUH Ahj: Bmepgo Zzxeudr tge Gekebj Oczidrew

Challenge

Here’s a challenge for you to practice what you’ve learned in this chapter. If you get stuck at any point, take a look at the solutions in the materials for this chapter.

Challenge: Fetch the article images from shared module

Instead of requesting the articles images from the UI, move this logic to the shared module.

Key points

  • A suspend function can only be called from another suspend function or from a coroutine.
  • You can use launch or async to create and start a coroutine.
  • A coroutine can start a thread from Main, IO or Default thread pools.
  • The new Kotlin/Native memory model gives you support to run multiple threads on iOS.

Where to go from here?

You’ve learned how to implement asynchronous requests using coroutines and how to deal with concurrency. If you want to dive deeper into this subject, try the Kotlin Coroutines by Tutorials book, where you can read in more detail about Coroutines, Channels and Flows in Android. There’s also Concurrency by Tutorials, which focuses on multithread in Swift, and Modern Concurrency in Swift, which teaches you the new concurrency model with async/away syntax.

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now