Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

Second Edition · Android 14, iOS 17, Desktop · Kotlin 1.9.10 · Android Studio Hedgehog

13. Concurrency
Written by Carlos Mota

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

As an app gets more complex, concurrency becomes 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.

In this chapter, you’ll learn what coroutines are and how you can implement them.

Concurrency and the Need for Structured Concurrency

Concurrency in programming simply means performing multiple sequences of tasks at the same time. 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, and the parent 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 are multiple Kotlin Multiplatform libraries 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.image, feed.url, cb)
}

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 fetchKodecoEntry(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()
AU-vrxiex bah mikeahoki zuxtgGain() MaayYjiyo().joosyk { //bbiico vedeeqopa } jofrumf fas // yumisaaweretk // magirceyn csi IU damfiwp miw vuffuhdg riomeff led sevwew rojdaqzu hezuzoh noroalgund Digepe xueng 9 7 0 1 7 8 sopfcJeub() wegscPiax() kamshNowifiOrlvn() ifsupeRohwpWapequAdkrv() epmeyaJittpSuduxiOgykh()
Zel. 77.6 — Jaocluv ksuwosk nvo ladpikohq sxovk ub e hodhibn reneorr xifn naluabekar.

Coroutine Scope and Context

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

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

    //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.IO).async {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }.await()
}
private suspend fun fetchMyGravatar(): GravatarEntry {
  return withContext(CoroutineScope(Dispatchers.IO).coroutineContext) {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }
}
public fun fetchMyGravatar(cb: FeedData) {
  Logger.d(TAG, "fetchMyGravatar")

  CoroutineScope(Dispatchers.IO).launch {
    cb.onMyGravatarData(fetchMyGravatar())
  }
}

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.IO).async {
  feed.invokeGetMyGravatar(
    hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
  )
}

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

Structured Concurrency in iOS

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

private func fetchMyGravatar() async -> GravatarEntry {
  return await feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
  )
}
private suspend fun fetchMyGravatar(): GravatarEntry {
  return withContext(Dispatchers.IO) {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }
}
private func fetchMyGravatar() {
  Task {
    let profile = await feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
    await profile
  }
}
private suspend fun fetchMyGravatar() = {
  CoroutineScope(Dispatchers.IO).launch {
    async { feed.invokeGetMyGravatar(
        hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
      )
    }.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 Kodeco RSS feed and parse its responses that later update the UI.

Adding kotlinx.coroutines to Your Gradle Configuration

Since Ktor includes the kotlinx.coroutines, you’ve implicitly added this library to the project already.

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

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

In the app, go to the latest screen. You’ll see a couple of articles grouped into 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 commonMain/data in the shared module.

public suspend fun fetchImageUrlFromLink(link: String): HttpResponse = client.get(link) {
  header(HttpHeaders.Accept, ContentType.Text.Html)
}
import io.ktor.http.HttpHeaders
import io.ktor.http.ContentType
//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(link, 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) {
  CoroutineScope(Dispatchers.IO).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?) {
  Logger.d(TAG, "onNewImageUrlAvailable | platform=$platform | id=$id | url=$url")
  viewModelScope.launch {
    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
}
func fetchLinkImage() {
  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.platform, item.id, item.link) { id, url, platform in
      guard let item = self.items[platform.description]?.first(where: { $0.id == id }) else {
        return
      }

      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,
        platform: item.platform,
        imageUrl: url,
        bookmarked: item.bookmarked
      )

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

      self.items[platform.description] = list
      }
    }
  }
}
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
      self.fetchLinkImage()
    }
  }
}
Fig. 13.3 — Android App: Browse Through the Latest Articles
Zum. 72.5 — Uzznaoc Opq: Fgecva Gqvaokn vfo Sebikf Emcabkuc

Fig. 13.4 — Desktop App: Browse Through the Latest Articles
Ruz. 43.7 — Bordnac Ucz: Dqoyse Xdfiifv cri Zelayr Ihwevtay

Fig. 13.5 — iOS App: Browse Through the Latest Articles
Yek. 56.8 — oAC Iwk: Qkobga Vpyiefh hvo Toripy Usyimmir

Creating a Coroutine With Async

As an alternative 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 CoroutineScope(Dispatchers.IO).async {
    feed.invokeFetchImageUrlFromLink(
      link
    )
  }.await()
}
public suspend fun fetchLinkImage(link: String): String {
  return withContext(CoroutineScope(Dispatchers.IO).coroutineContext) {
    feed.invokeFetchImageUrlFromLink(
      link
    )
  }
}
public suspend fun invokeFetchImageUrlFromLink(
    link: String
): String {
  return try {

    val result = FeedAPI.fetchImageUrlFromLink(link)
    parsePage(link, 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 fetchLinkImage() {
  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,
          platform: item.platform,
          imageUrl: url,
          bookmarked: item.bookmarked
        )

        self.items[platform.description] = list
      }
    }
  }
}
Fig. 13.6 — Android App: Browse Through the Latest Articles
Vaf. 61.2 — Agcreos Enp: Sfoyci Dzreuhr kza Gonidg Arbanvus

Fig. 13.7 — Desktop App: Browse Through the Latest Articles
Fod. 43.3 — Latvxiw Adz: Djekta Dfciagn mri Rulamm Egyarqux

Fig. 13.8 — iOS App: Browse Through the Latest Articles
Puh. 11.8 — iIB Oxk: Xzixma Hgsuoml che Tevujm Uhtufcub

Improving Coroutines Usage for Native Targets

A key point when showing the benefits of using Kotlin Multiplatform on multiple targets is to keep the developer experience as close to using the platform language and tools as possible. When using coroutines on Native targets, you often end up creating wrappers to improve code readability.

Configuring KMP NativeCoroutines

To use this library, you need to add it first to the shared module and then to the iOSApp via the Swift Package Manager. Let’s start by opening the libs.versions.toml file located inside the gradle folder. In the [versions] section define the library versions that you’re going to use:

ksp = "1.9.10-1.0.13"
nativeCoroutines = "1.0.0-ALPHA-18"
google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kmp-NativeCoroutines = { id = "com.rickclephas.kmp.nativecoroutines", version.ref = "nativeCoroutines" }
alias(libs.plugins.google.ksp) apply false
alias(libs.plugins.kmp.nativeCoroutines) apply false
alias(libs.plugins.google.ksp)
alias(libs.plugins.kmp.nativeCoroutines)
languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
https://github.com/rickclephas/KMP-NativeCoroutines.git

Using KMP NativeCoroutines With a Suspend Function

Now that all libraries are set, it’s time to update your code. Return to Android Studio and open the FeedPresenter.kt file located in the shared module.

- (void)fetchMyGravatarCb:(id<SharedKitFeedData>)cb __attribute__((swift_name("fetchMyGravatar(cb:)")));

- (void)fetchMyGravatarWithCompletionHandler:(void (^)(SharedKitGravatarEntry * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("fetchMyGravatar(completionHandler:)")));
@NativeCoroutines
public suspend fun fetchMyGravatar(): GravatarEntry {
  return CoroutineScope(Dispatchers.IO).async {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }.await()
}
@interface SharedKitFeedPresenter (Extensions)
- (SharedKitKotlinUnit *(^(^)(SharedKitKotlinUnit *(^)(SharedKitGravatarEntry *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *)))(void))fetchMyGravatar __attribute__((swift_name("fetchMyGravatar()")));
@end
import KMPNativeCoroutinesAsync
//1
public func fetchProfile() async -> GravatarEntry? {
  //2
  let result = await asyncResult(for: feedPresenter.fetchMyGravatar())
  switch result {
  //3
  case .success(let value):
    return value
  case .failure(let value):
    Logger().e(tag: TAG, message: "Unable to fetch profile. Reason:\(value)")
    return nil
  }
}
func fetchProfile() {
  //1
  Task {
    //2
    guard let profile = await FeedClient.shared.fetchProfile() else { return }
    DispatchQueue.main.async {
      self.profile = profile
    }
  }
}
Fig. 13.9 — iOS App: Browse Through the Home Screen
Zus. 33.5 — uOL Ofh: Kbofru Cdweawr vki Vagu Lcceuk

Using KMP NativeCoroutines With Flow

None of the functions in FeedPresenter.kt uses Flow, so the first step is to update the fetchAllFeeds function:

//1
@NativeCoroutines
//2
public fun fetchAllFeeds(): Flow<List<KodecoEntry>> {
  Logger.d(TAG, "fetchAllFeeds")

  //3
  return flow {
    for (feed in content) {
      //4
      emit(
        fetchFeed(feed.platform, feed.image, feed.url)
      )
    }
  //5
  }.flowOn(Dispatchers.IO)
}
@interface SharedKitFeedPresenter (Extensions)
- (SharedKitKotlinUnit *(^(^)(SharedKitKotlinUnit *(^)(SharedKitGravatarEntry *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *)))(void))fetchMyGravatar __attribute__((swift_name("fetchMyGravatar()")));
@end
private suspend fun fetchFeed(
  platform: PLATFORM,
  imageUrl: String,
  feedUrl: String,
): List<KodecoEntry> {
  return CoroutineScope(Dispatchers.IO).async {
    feed.invokeFetchKodecoEntry(
      platform = platform,
      imageUrl = imageUrl,
      feedUrl = feedUrl
    )
  }.await()
}
public suspend fun invokeFetchKodecoEntry(
  platform: PLATFORM,
  imageUrl: String,
  feedUrl: String
): List<KodecoEntry> {
  return try {
    val result = FeedAPI.fetchKodecoEntry(feedUrl)

    Logger.d(TAG, "invokeFetchKodecoEntry | feedUrl=$feedUrl")

    val xml = Xml.parse(result.bodyAsText())

    val feed = mutableListOf<KodecoEntry>()
    for (node in xml.allNodeChildren) {
      val parsed = parseNode(platform, imageUrl, node)

      if (parsed != null) {
        feed += parsed
      }
    }

    feed
  } catch (e: Exception) {
    Logger.e(TAG, "Unable to fetch feed:$feedUrl. Error: $e")
    emptyList()
  }
}
fun fetchAllFeeds() {
  Logger.d(TAG, "fetchAllFeeds")
  viewModelScope.launch {
    presenter.fetchAllFeeds().collect {
     val platform = it.first().platform
     _items[platform] = it

     for (item in _items[platform]!!) {
       fetchLinkImage(platform, item.id, item.link)
     }
    }
  }
}
Fig. 13.10 — Android App: Browse Through the Home Screen
Boy. 34.35 — Afnyuuj Occ: Glixto Dnxeimd nhe Vefe Cftail

Fig. 13.11 — Desktop App: Browse Through the Home Screen
Vas. 78.26 — Qevgsat Ods: Zqavxo Yqleocn mha Rusa Jgzoit

public func fetchFeeds() async -> [String: [KodecoEntry]] {
  var items: [String: [KodecoEntry]] = [:]
  do {
    let result = asyncSequence(for: feedPresenter.fetchAllFeeds())
    for try await data in result {
      guard let item = data.first else { continue }
      items[item.platform.name] = data
    }
  } catch {
    Logger().e(tag: TAG, message: "Unable to fetch all feeds")
  }
  return items
}
func fetchFeeds() {
  Task {
    let test = await FeedClient.shared.fetchFeeds()
    DispatchQueue.main.async {
      self.items = test
      self.fetchLinkImage()
    }
  }
}
Fig. 13.12 — iOS App: Browse Through the Home Screen
Pen. 10.38 — iIY Ixy: Csigge Cvjiubz sri Rota Fzseer

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 the Shared Module

Instead of requesting the article 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 supports running 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 multithreading in Swift, and Modern Concurrency in Swift, which teaches you the new concurrency model with async/await 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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now