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

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:

  • kotlinx.coroutines: The most popular one. It’s lightweight, it allows running multiple coroutines in a single thread and it supports exception handling and cancellation.
  • Reaktive: An implementation of Reactive Extensions using the Observable pattern.
  • CoroutineWorker: Supports multithreaded coroutines.

In this chapter, you’ll learn how to use kotlinx.coroutines. Spoiler alert: You’ve already worked with coroutines before. :]

If you’re already familiar with coroutines, you can skip the next few sections and go to “Structured concurrency in iOS”, or directly to “Working with kotlinx.coroutines”, for the next developments in learn.

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.

Open the FeedPresenter.kt file from shared/commonMain/presentation and search for fetchAllFeeds and fetchFeed. In the first function, you’ve got:

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

If you weren’t using coroutines on fetchFeed, these instructions would run sequentially. In other words, the app would only iterate to the next item after fetchFeed returned, which would delay the app startup.

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.

Network requests are one of the use cases for suspend functions. Open the FeedAPI.kt file in shared/commonMain/data and look at the function declarations:

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()

They’re both suspend functions. Since a response may take some time, the app cannot block and wait for any of these functions to return.

This image defines the flow that triggers fetchKodecoEntry to be called.

UI-thread new coroutine fetchFeed() MainScope().launch { //create coroutine } suspend fun // deserializing // notifying the UI suspend fun suspends waiting for server response resumes requesting Kodeco feeds 1 2 3 5 4 6 fetchFeed() fetchFeed() fetchKodecoEntry() invokeFetchKodecoEntry() invokeFetchKodecoEntry()
Fig. 13.2 — Diagram showing the different steps of a network request with coroutines.

The entry point, for all platforms, is the fetchAllFeeds function from shared/commonMain/presentation/FeedPresenter.kt. Once invoked, it iterates over all the RSS feeds, calling fetchFeed for each one of its URLs:

  1. This is a heavy operation that might block the UI. To avoid this, you’ll do it asynchronously. Create a coroutine by calling launch.
  2. Once launched, it calls invokeFetchKodecoEntry from shared/commonMain/domain/GetFeedData.kt. A suspend function calls the FeedAPI to make the request.
  3. This function suspends after making the request, and it waits until there’s a response or the connection times out.
  4. This is done in a separate thread, so the UI doesn’t get blocked.
  5. Once there’s a response, fetchKodecoEntry resumes and returns to invokeFetchKodecoEntry, which can now deserialize the information received.
  6. When this process finishes, the onSuccess or the onFailure functions execute — depending on the result — and the UI receives an update. Since you’re using MainScope to launch the coroutine, it will run on the UI thread. You’ll see this in detail in the next section.

As a key point, you can only call a suspend function from another one or within a coroutine.

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
  }
}

You already know that launch creates a new coroutine, but what’s MainScope? A coroutine scope is where a coroutine will run — in this case, it will be the main thread.

If you open the source code of MainScope:

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

You can see that ContextScope is built using:

  • SupervisorJob: When you create a coroutine, it returns a Job that corresponds to its instance. This allows you to cancel the coroutine or to know more about its current state:
    • isActive: If it’s currently running.
    • isCompleted: If all of its work, as well as that of its children, has ended. Moreover, when the current Job gets canceled — or fails — this value will be true.
    • isCancelled: When the current job gets canceled or fails.

In a SupervisorJob, the children behave independently — if one fails, the others won’t be affected — whereas in case of Job, if a parent fails, all of its children will be canceled.

  • Dispatchers: Defines in which thread a coroutine should run:
    • Default: Uses a shared pool of threads.
    • Main: Has different behaviors depending on the platform it’s running on. On JVM and Android, Main corresponds to the UI thread, and should only be used for operations that update the UI. On Native, it depends on the target itself. If it’s Darwin-based, the dispatcher is backed by Darwin’s main queue. For the other targets, it’s the same as the Default dispatcher.
    • Unconfined: Doesn’t have any associated threading policy and doesn’t switch to any specific thread.
    • IO: Should be used for long-running and heavy tasks because it’s one shared pool of threads, optimized for these types of operations.

When you create a coroutine, you have to define the Dispatcher where it should run, but you can always switch the context later on during execution by calling withContext with the prepended Dispatcher as an argument.

In the fetchMyGravatar from FeedPresenter.kt, you’re running the coroutine on the main thread, although the only parts that are necessary to run on the main thread are the onSuccess and onFailure calls. Update the existing function to use the IO thread for the network requests and when the data is available, switch to the Main thread so the UI can be updated:

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)
    }
  }
}

Here’s what you’re doing:

  1. You create a new coroutine in a thread from the IO thread pool and start it.

  2. invokeGetMyGravatar is a suspend function. When there’s a request, it suspends until there’s a server response. Once this happens, the coroutine resumes.

  3. The UI can only be updated from the UI thread, so it’s necessary to switch from the IO dispatcher to the Main one. This can only be done from within a coroutine.

  4. onMyGravatarData is now called from the UI thread, so the user can see this newly received data.

You’ll also need to update the invokeGetMyGravatar function to return the result instead. Open the GetFeedData.kt file from commonMain/domain and change invokeGetMyGravatar to:

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()
  }
}

In addition to MainScope, you also have GlobalScope. Typically, it’s used in scenarios where the coroutine must live throughout the app execution.

You have to be extra careful when using this function. If the coroutine is unable to finish, it will keep using resources, potentially until the user closes the app.

If you have to update the UI, and you’re using GlobalScope, you must switch to the UI thread first. Otherwise, when running your iOS app, you’ll get the following exception:

kotlin.native.IncorrectDereferenceException: illegal attempt to access non-shared (…) from other thread

You also have the coroutineScope function that allows you to create a coroutine, but it uses the parent scope as context. It has some particularities, namely:

  • If the parent gets canceled, it will cancel all of its children.
  • Only after all the children end can the parent also terminate.

Coroutine Builders, Scope and Context

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

  • runBlocking: blocks the current thread until the coroutine that it creates ends.

Note: It shouldn’t be used inside an existing coroutine, since it will stop its execution.

  • launch: Creates a coroutine without blocking the current thread. You can define the CoroutineScope from where it should run. This scope guarantees structured concurrency — in other words, a coroutine only ends after all of its children have completed their operations.

  • async: Similar to launch in the way it’s constructed and how it runs. It differs on its return type in that in this case it’s not a Job, but a Deferred<T> object that will contain the future result of this function.

Return to the fetchMyGravatar function and add this function below it:

private suspend fun fetchMyGravatar(): GravatarEntry {
  return CoroutineScope(Dispatchers.IO).async {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }.await()
}

This fetchMyGravatar is a suspend function. With this approach, you don’t need the onSuccess and onFailure callbacks to update the UI, since you’ll return a GravatarEntry. You need to call await at the end to return its final value instead of a Deferred<GravatarEntry>.

It’s worth mentioning that this function is similar to using withContext:

private suspend fun fetchMyGravatar(): GravatarEntry {
  return withContext(CoroutineScope(Dispatchers.IO).coroutineContext) {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }
}

The main difference is that CoroutineScope doesn’t use the same scope as its caller.

Following this approach means that you’ll also have to make a few more updates. To use the same logic to notify the UI via callbacks, you’ll need to change fetchMyGravatar(cb: FeedData) to:

public fun fetchMyGravatar(cb: FeedData) {
  Logger.d(TAG, "fetchMyGravatar")

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

Otherwise, you can return the GravatarEntry directly to the UI. You’ll see how to implement this second approach in the “Creating a Coroutine With Async” section.

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.

In case you’re using async, you’ll have to implement a solution similar to this one:

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

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

When you cancel a coroutine, a CancellationException is thrown silently. You can catch it to implement a specific behavior your app might need, or to clean up resources.

Structured Concurrency in iOS

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

Note: async/await is only available if you’re running your app on iOS 13 or newer versions.

With async/await, you no longer need to use completion handlers. Instead, you can use the async keyword after the function declaration. If you want to wait for it to return, add await before calling the suspend function:

private func fetchMyGravatar() async -> GravatarEntry {
  return await feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
  )
}

Which in Kotlin is similar to:

private suspend fun fetchMyGravatar(): GravatarEntry {
  return withContext(Dispatchers.IO) {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }
}

Following the same logic as suspend functions, you can only call an async function from another one or from an asynchronous task. In Kotlin, this corresponds to calling the function from a coroutine.

Swift uses Task. Using Task, the previous example can be translated to:

private func fetchMyGravatar() {
  Task {
    let profile = await feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
    await profile
  }
}

With kotlinx.coroutines, it’s:

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.

However, there’s a little detail that was left for this section: Ktor is built using kotlinx.coroutines. This is why the MainScope, launch and suspend functions seemed familiar in the “Understanding kotlinx.coroutines” section.

Adding kotlinx.coroutines to Your Gradle Configuration

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

Otherwise, if you want to include kotlinx.coroutines in your projects, you’ll need to add:

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:

Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen

This InvalidMutabilityException means you’re accessing an object that belongs to another thread, which is currently not possible. Confirm if you’re using the latest version of Kotlin and that you’re using the Dispatchers.Main to access that object.

If you’re still running into problems:

  • Delete the build folder in the root directory of the project.
  • Delete the build folder in the shared directory in the root directory of the project.

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.

Another advantage of using the kotlinx.coroutines library is that this logic is already built into the latest version of the library, so you shouldn’t need to do anything from your side.

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.

After the fetchKodecoEntry, add:

public suspend fun fetchImageUrlFromLink(link: String): HttpResponse = client.get(link) {
  header(HttpHeaders.Accept, ContentType.Text.Html)
}

When prompted, import:

import io.ktor.http.HttpHeaders
import io.ktor.http.ContentType

fetchImageUrlFromLink receives the link from an article and returns the page source code as the HttpResponse. It’s set as a suspend function, so the current thread won’t block while it’s waiting for the server response.

Note: You need to set the Accept header in this request otherwise the server will return a 406, not acceptable.

Next, open the GetFeedData.kt file from shared/commonMain/domain and add the following method inside the class:

//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)
    }
  }
}

Here’s a step-by-step breakdown of this logic:

  1. invokeFetchImageUrlFromLink is set as suspend since it will call the FeedAPI to retrieve the page source code.
  2. The onSuccess and onFailure functions define how this function should behave, depending on whether it was possible to retrieve an image for the article or not.
  3. The FeedAPI uses the Ktor HttpClient to make a network request.
  4. Since there’s no API to get the URL for the image, you’re going to parse the HTML code and look for a specific image tag. Along with the network request, this will be a heavy task. So, this logic needs to be called from a coroutine.
  5. The coroutineScope creates a new coroutine, using its parent scope to run the functions of onSuccess or onFailure depending on whether the operation succeeded or not.

In the next sections, you’ll see different approaches to creating and starting a coroutine. Although both of them are valid, the APIs that they expose to the UI are different.

Note: When multiple teams will use a shared module, it’s best for representatives from each team to discuss and agree on conventions they all feel comfortable following. This is especially important for iOS programmers who are new to Kotlin and can feel overwhelmed having to adapt to a new language. Interacting with your shared module should be similar to any other library that exists for iOS.

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. :]

Open the FeedPresenter.kt file inside commonMain/presentation. In the shared module and before the fetchMyGravatar(cb: FeedData) function, add:

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) }
    )
  }
}

In this approach, you’re using a FeedData listener that’s defined at the UI level. Once the invokeFetchImageUrlFrom finishes, it will either call the onSuccess or onFailure functions that in their turn will call the onNewImageUrlAvailable callback at the UI with the new data received or with an exception in case there was an error.

Now, connect your app’s UI to this new function.

In androidApp and desktopApp, the changes are similar. In both projects, go to ui/home, open the FeedViewModel.kt file, and update the onNewImageUrlAvailable callback with:

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
  }
}

When this method receives a new url, the item to which it corresponds is updated. Updating the _items map automatically updates the UI.

Note: viewModelScope runs on the UI-thread.

Inside the viewModelScope.launch of onNewDataAvailable, replace the existing code with:

_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)
}

Now, when the app receives new articles, it will automatically request its images.

Create the fetchLinkImage function:

private fun fetchLinkImage(platform: PLATFORM, id: String, link: String) {
  Logger.d(TAG, "fetchLinkImage | link=$link")
  presenter.fetchLinkImage(platform, id, link, this)
}

fetchLinkImage calls the fetchLinkImage from the FeedPresenter.kt file that you created earlier.

In the iosApp, open the FeedClient.swift file that’s inside the extensions directory and search for fetchLinkImage. To also call the fetchLinkImage from the FeedPresenter.kt class, update this function to:

public func fetchLinkImage(_ platform: PLATFORM, _ id: String, _ link: String, completion: @escaping FeedHandlerImage) {
  feedPresenter.fetchLinkImage(platform: platform, id: id, link: link, cb: self)
  handlerImage = completion
}

Now, navigate to KodecoEntryViewModel.swift and, similar to what you’ve done on the other platforms, create the fetchLinkImage:

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
      }
    }
  }
}

Finally, call it from the existing the fetchFeeds function as follows:

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()
    }
  }
}

Compile and run the apps for the three platforms and navigate to the latest screen.

Fig. 13.3 — Android App: Browse Through the Latest Articles
Fig. 13.3 — Android App: Browse Through the Latest Articles

Fig. 13.4 — Desktop App: Browse Through the Latest Articles
Fig. 13.4 — Desktop App: Browse Through the Latest Articles

Fig. 13.5 — iOS App: Browse Through the Latest Articles
Fig. 13.5 — iOS App: Browse Through the Latest Articles

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.

Return to the FeedPresenter.kt file in commonMain/presentation in the shared module, and update the function fetchLinkImage:

public suspend fun fetchLinkImage(link: String): String {
  return CoroutineScope(Dispatchers.IO).async {
    feed.invokeFetchImageUrlFromLink(
      link
    )
  }.await()
}

As you can see, it’s no longer necessary to have the platform and id parameters, since you’re going to return the image URL in case it exists. The async function allows returning an object while await waits for the response to be ready. Instead of returning a Deferred<T> — in this case it would be a Deferred<String?>.

Depending on the Android Studio version you’re using, it’s probable that it would suggest you replace the previous implementation with:

public suspend fun fetchLinkImage(link: String): String {
  return withContext(CoroutineScope(Dispatchers.IO).coroutineContext) {
    feed.invokeFetchImageUrlFromLink(
      link
    )
  }
}

Both approaches produce similar results, but they’re quite different under the hood.

You can remove the onNewImageUrlAvailable from the FeedData.kt interface, located in the domain/cb directory.

Open GetFeedData.kt and update invokeFetchImageUrlFromLink to the following:

public suspend fun invokeFetchImageUrlFromLink(
    link: String
): String {
  return try {

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

  } catch (e: Exception) {
    ""
  }
}

Now it’s time to update the UI! You’ll need to change how you’re calling the fetchLinkImage function:

  • On both androidApp and desktopApp, go to the FeedViewModel.kt file inside ui/home, and replace the existing fetchLinkImage function with:
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
  }
}

This is the code that was in onNewImageUrlAvailable, along with the call to presenter.fetchLinkImage. Since you no longer use that callback, you can remove it.

  • For iOSApp, you also need to update the FeedClient.swift file, which is inside the extensions folder. Start by updating the FeedHandlerImage that no longer has to receive all of its parameters:
public typealias FeedHandlerImage = (_ url: String) -> Void

Update the fetchLinkImage to:

@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")
    }
  }
}

Since you’re now accessing a suspend function from Swift, you’ll have to use await to wait for the result to be available. The @MainActor annotation guarantees the Task runs on the UI thread. Otherwise, you might have a InvalidMutabilityException.

Now, remove the onNewImageUrlAvailable from the FeedClient extension at the bottom of the file since this callback no longer exists.

Because this function needs to be declared as @MainActor and the id, platform and cb are no longer necessary, you have to update the fetchLinkImage method from KodecoEntryViewModel.swift :

@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
      }
    }
  }
}

Compile and run your app, and browse through the outstanding artwork of the Kodeco articles. :]

Fig. 13.6 — Android App: Browse Through the Latest Articles
Fig. 13.6 — Android App: Browse Through the Latest Articles

Fig. 13.7 — Desktop App: Browse Through the Latest Articles
Fig. 13.7 — Desktop App: Browse Through the Latest Articles

Fig. 13.8 — iOS App: Browse Through the Latest Articles
Fig. 13.8 — iOS App: Browse Through the Latest Articles

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.

The following libraries were developed by the community to improve coroutines integration with Swift:

  • KMP-NativeCoroutines: allows canceling an existing coroutine from Swift and adds support to Flow without losing any property. It’s recommended by JetBrains and it gets continuous updates.

  • Koru: supports generating wrappers for suspend functions and Flow.

  • SKIE: has the primary goal of making it seamless to use Kotlin from Swift by providing support for suspend functions, Flow and default arguments. It also makes it easier to use Kotlin enums, sealed classes and interfaces.

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"

Then scroll down to [plugins] and add:

google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kmp-NativeCoroutines = { id = "com.rickclephas.kmp.nativecoroutines", version.ref = "nativeCoroutines" }

KSP (Kotlin Symbol Processing API) is required by KMP NativeCoroutines.

Now, open the build.gradle.kts file in the root folder and update the plugins sections with these new ones:

alias(libs.plugins.google.ksp) apply false
alias(libs.plugins.kmp.nativeCoroutines) apply false

shared will use this library, so you also need to set the plugin on its build.gradle.kts file. In the plugins block, add:

alias(libs.plugins.google.ksp)
alias(libs.plugins.kmp.nativeCoroutines)

To generate the wrappers for Native, you’ll need to opt in to use the annotation ObjCName, otherwise, you’ll keep seeing a warning when compiling your project. Scroll down to the end of the file and after the existing kotlin.RequiresOptIn add a second one:

languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")

Synchronize the project.

Open Xcode and go to FileAdd Package Dependencies… and in the top-right corner where it says, “Search or Enter Package URL” enter:

https://github.com/rickclephas/KMP-NativeCoroutines.git

Click Add Package to add it to your app.

That’s it! All libraries are set.

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.

KMP NativeCoroutines, has two annotations that you can use:

  • @NativeCoroutineScope: it defines the scope to use, and allows you to have more control over it.
  • @NativeCoroutines: functions that use this are not generated for Objective-C, but instead, an extension is created that can more easily be accessed from Swift. This is generated via the KSP plugin that you just added.

You can open the SharedKit.framework directly from Xcode by clicking on import SharedKit or any function from it in any Swift file that uses it, or alternatively, you can go to shared/build/bin/ios*Arm64/debugFramework/Headers/SharedKit.h.

Look for the fetchMyGravatar declaration. Here you’re going to find two of them, one that receives a completion handler and another one that receives a callback.

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

- (void)fetchMyGravatarWithCompletionHandler:(void (^)(SharedKitGravatarEntry * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("fetchMyGravatar(completionHandler:)")));

To use these functions from Swift, you need to create a handler and an extension for retrieving data, as you can see in FeedClient.swift. With the KMP NativeCoroutines library, the generated code can be improved, making it easier and more friendly to access. For that, return to the FeedPresenter.kt file and update fetchMyGravatar(): GravatarEntry:

@NativeCoroutines
public suspend fun fetchMyGravatar(): GravatarEntry {
  return CoroutineScope(Dispatchers.IO).async {
    feed.invokeGetMyGravatar(
      hash = GRAVATAR_EMAIL.toByteArray().md5().toString()
    )
  }.await()
}

It’s now set as public and has the @NativeCoroutines annotation set.

Compile the project and open the SharedKit.framework again. You’ll see that there’s no longer a fetchMyGravatarWithCompletionHandler function, but instead, you’ve got an interface with fetchMyGravatar declared, that you can easily call from Swift:

@interface SharedKitFeedPresenter (Extensions)
- (SharedKitKotlinUnit *(^(^)(SharedKitKotlinUnit *(^)(SharedKitGravatarEntry *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *)))(void))fetchMyGravatar __attribute__((swift_name("fetchMyGravatar()")));
@end

Now open the FeedClient.swift file and import the KMPNativeCoroutinesAsync library:

import KMPNativeCoroutinesAsync

Afterward, update the fetchProfile function to:

//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
  }
}

Here’s a step-by-step breakdown of this logic:

  1. You’re going to access a function that accesses the internet to retrieve the user’s Gravatar information. To accomplish this, fetchMyGravatar is set as a suspend function and returns a GravatarEntry. This is an asynchronous operation so fetchMyProfile declaration needs to reflect that, this is why it’s now set as async.
  2. asyncResult is a wrapper from KMPNativeCoroutinesAsync that returns the result of a function along with the operation state: .success or .failure.
  3. If the call is successful and new data is available, its content is returned. Otherwise, a log message is printed, and the result is nil.

With this change, you can now safely remove the ProfileHandler declaration and all of its usage.

Since the behavior of this function changed, you also need to update its caller; otherwise, the project won’t compile. Open the KodecoEntryViewModel.kt file and look for fetchProfile, update it to reflect these changes:

func fetchProfile() {
  //1
  Task {
    //2
    guard let profile = await FeedClient.shared.fetchProfile() else { return }
    DispatchQueue.main.async {
      self.profile = profile
    }
  }
}

Let’s break this code snippet into parts:

  1. Since fetchProfile is asynchronous, to avoid blocking the main thread, this call will be executed inside a Task.
  2. If the result is .success, the main thread is resumed and the self.profile updated. Otherwise, the Task ends.

All done! Compile and run the project.

Fig. 13.9 — iOS App: Browse Through the Home Screen
Fig. 13.9 — iOS App: Browse Through the Home Screen

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)
}

There are a couple of changes to this function, so let’s go over them step-by-step:

  1. Similar to before, the use of the @NativeCoroutines annotation allows generating an interface with the fetchAllFeeds function which is easier and more friendly to call from Swift:
@interface SharedKitFeedPresenter (Extensions)
- (SharedKitKotlinUnit *(^(^)(SharedKitKotlinUnit *(^)(SharedKitGravatarEntry *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *), SharedKitKotlinUnit *(^)(NSError *, SharedKitKotlinUnit *)))(void))fetchMyGravatar __attribute__((swift_name("fetchMyGravatar()")));
@end
  1. Previously, fetchAllFeeds would receive a callback, which on Native would be translated to a completion handler. The first step is to remove this argument, and instead return a Flow with the list of all the KodecoEntry. Ideally, it would return a Map<PLATFORM, List<KodecoEntry>>, but this is currently not possible with the current version of KMP NativeCoroutines.
  2. The Flow that your function will return.
  3. For each Kodeco topic: All, Android, iOS, Flutter, Server, GameTech, and Growth, you’re going to retrieve its RSS feed. Once this data is available, it will be emitted automatically, to whoever is listening to it. In this case, it will be FeedViewModel.kt on Android and Desktop and KodecoEntryViewModel.swift on iOS.
  4. Finally, this will run on the IO dispatcher.

With the removal of the FeedData callback from fetchAllFeeds you need to do the same thing on the fetchFeed function called in step 4. Update it to:

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()
}

Now, instead of receiving cb it returns the list of KodecoEntry that corresponds to the RSS feed for a specific topic. This change, also requires that invokeFetchKodecoEntry be modified. Open the GetFeedData.kt file and update this function to:

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()
  }
}

Similarly, as before, instead of calling the cb with the result of the network requests, it will return a list of RSS feeds in case it successfully received and parsed them or an empty list in case any of these operations fail.

Since you’re now returning the data directly, you can remove the onNewDataAvailable function from FeedData.kt.

With the shared logic updated, it’s now time to update the apps. Starting with Android, open the FeedViewModel.kt file and replace the existing fetchAllFeeds function to:

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)
     }
    }
  }
}

And remove the onNewDataAvailable function. Now that you’ve got the Android app ready, repeat both steps for Desktop.

Build and run to see the result of the refactoring that you just did.

Fig. 13.10 — Android App: Browse Through the Home Screen
Fig. 13.10 — Android App: Browse Through the Home Screen

Fig. 13.11 — Desktop App: Browse Through the Home Screen
Fig. 13.11 — Desktop App: Browse Through the Home Screen

Return to Xcode and open the FeedClient.swift file. Similarly, to what you’ve changed in the previous section, you need to update the fetchFeeds function to return a dictionary with the platform name and the list of KodecoEntry:

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
}
  1. KMPNativeCoroutinesAsync has different functions depending on the type of data that you’re going to access. For Flow you need to use the asyncSequence that allows you to collect the values from it.
  2. Since it’s an asynchronous operation, you need to wait for it to return, or in other words, to emit (from fetchAllFeeds on FeedPresenter.kt) the data that you’re expecting.
  3. If data is valid, you’re going to update the current items list, otherwise it’s discarded.

Remove the onNewDataAvailable extension function and the FeedHandler declaration and usage, which are no longer necessary.

Finally, open the KodecoEntryViewModel.swift and update the fetchFeeds function:

func fetchFeeds() {
  Task {
    let test = await FeedClient.shared.fetchFeeds()
    DispatchQueue.main.async {
      self.items = test
      self.fetchLinkImage()
    }
  }
}

It creates a task to avoid blocking the main thread. Once new data is available it returns to it and updates self.items which in turn notifies the app that there’s new content to update. After the RSS feed is received, the app fetches its corresponding images.

Compile and run the app.

Fig. 13.12 — iOS App: Browse Through the Home Screen
Fig. 13.12 — iOS App: Browse Through the Home Screen

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.

Remember that you don’t need to run this logic sequentially — you can launch multiple coroutines to fetch and parse the response, making this operation faster.

The requests should run in parallel.

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.

In the next chapter, you’ll learn how to migrate a feature to support Kotlin Multiplatform and release your libraries so that you can later reuse them in your projects.

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