13.
Concurrency
Written by Carlos Mota
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.
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:
- kotlinx.coroutines: The most popular one, mostly because of its use among Android developers and recommendation from JetBrains and Google. 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.url)
}
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 on shared/commonMain/data and look at the functions’ declaration:
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()
They’re all 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 fetchRWEntry to be called.
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, and calls fetchFeed for each one of its URLs:
- This is a heavy operation that might block the UI. To avoid this, you’ll do it asynchronously. Create a coroutine by calling
launch. - Once launched, it calls
invokeFetchRWEntryfrom shared/commonMain/domain/GetFeedData.kt. A suspend function calls the FeedAPI to make the request. - This function suspends after making the request, and it waits until there’s a response or the connection times out.
- This is done in a separate thread, so the UI doesn’t get blocked.
- Once there’s a response,
fetchRWEntryresumes and returns toinvokeFetchRWEntry, which can now deserialize the information received. - When this process finishes, the
onSuccessor theonFailurefunctions execute — depending on the result — and the UI receives an update. Since you’re usingMainScopetolaunchthe coroutine, this means 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, feedUrl: String, cb: FeedData) {
MainScope().launch {
// Call to invokeFetchRWEntry
}
}
You already know that launch creates a new coroutine, but what’s MainScope? A coroutine scope is where a coroutine is going to 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 its ContextScope is built using:
-
SupervisorJob: When you create a coroutine, it returns aJobthat corresponds to its instance. This allows you to cancel or to know more about its current state:-
isActive: If it’s currently running. -
isCompleted: If all of its work, as well as its children, have ended. Moreover, when the current Job gets cancelled — or fails — its value will be true. -
isCancelled: When the current job gets cancelled or fails.
-
The difference between a SupervisorJob and a Job is that they have different policies. In the first one, the children behave independently — if one fails, the others won’t be affected — whereas in the second one, if a parent fails, all of its children will be cancelled.
-
Dispatchers: Define in which thread a coroutine should run:-
Default: Uses a shared pool of threads. -
Main: Has different behaviors depending on the platform that’s currently running. On JVM and Android,Maincorresponds to the UI-thread, and should only be used for operations that update the UI. On Native, it’s the same as theDefaultdispatcher. -
Unconfined: Doesn’t have any associated threading policy. 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. It’s currently not available for iOS.
-
When you create a coroutine, you have to define the dispatcher where it should run, but you can always switch the context later on its execution by calling withContextwith the prepended Dispatcher as an argument.
In the fetchMyGravatar function from FeedPresenter.kt, you’re running the coroutine in the main thread, although the only part that is necessary to run are the onSuccess and onFailure calls. You can update the existing function to use the Default 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.Default).launch {
//2
val profile = feed.invokeGetMyGravatar(
hash = md5(GRAVATAR_EMAIL)
)
//3
withContext(Dispatchers.Main) {
//4
cb.onMyGravatarData(profile)
}
}
}
Here’s what you’re doing:
- Creates a new coroutine in a thread from the Default thread-pool and starts it. Ideally, you should use the IO dispatcher. However, it isn’t supported in iOS, so you’ll need to create a platform-specific logic for this, as you’ll see in the “Implementing Dispatchers: IO for iOS” section.
-
invokeGetMyGravataris a suspend function. When there’s a request, it suspends until there’s a server response. Once this happens, the coroutine resumes. - The UI can only be updated from the UI-thread, so it’s necessary to switch from the
Defaultdispatcher to theMainone. This can only be done from within a coroutine. -
onMyGravatarDatais 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 it 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 on 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 before. 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 cancelled, 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 structure concurrency — in other words, a coroutine only ends after all of its children have completed their operations. -
async: Similar tolaunchin 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 it’s a Deferred<T> object that will contain the future result of this function.
Return to the fetchMyGravatar function and add below:
private suspend fun fetchMyGravatar(): GravatarEntry {
return CoroutineScope(Dispatchers.Default).async {
feed.invokeGetMyGravatar(
hash = md5(GRAVATAR_EMAIL)
)
}.await()
}
fetchMyGravatar is now a suspend function. With this approach, you don’t need the onSuccess and onFailure callbacks to update the UI, since you’re going to 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 use withContext:
private suspend fun fetchMyGravatar(): GravatarEntry {
return withContext(CoroutineScope(Dispatchers.Default).coroutineContext) {
feed.invokeGetMyGravatar(
hash = md5(GRAVATAR_EMAIL)
)
}
}
The main difference between both calls 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.Default).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.
With this change, you need to update the calling function fetchProfile from RWEntryViewModel on the iOS app so the UI can be successfully updated:
func fetchProfile() {
FeedClient.shared.fetchProfile { profile in
Logger().d(tag: TAG, message: "fetchProfile: \(profile)")
DispatchQueue.main.async {
self.profile = profile
}
}
}
You don’t need to update the Android app or the desktop app, since the viewModelScope runs on the UI thread.
Note: In the next sections, you’ll learn that iOS is single-threaded by default. Only when you enable the new Kotlin/Native memory model, you’re able to use multi-threading. With this, if you want to compile your app now, you need to replace
Dispatchers.DefaultwithDispatchers.Main. Alternatively, you can implement the dispatcher at the platform-specific level, as you’ll see in the section “Implementing Dispatchers: IO for iOS.”
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.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()
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 using Xcode 13.2 or later and 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 = md5(GRAVATAR_EMAIL)
)
}
Which is similar in Kotlin to:
private suspend fun fetchMyGravatar(): GravatarEntry {
return withContext(Dispatchers.IO) {
feed.invokeGetMyGravatar(
hash = md5(GRAVATAR_EMAIL)
)
}
}
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 = md5(GRAVATAR_EMAIL)
)
await profile
}
}
With kotlinx.coroutines, it’s:
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.
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, when you added this library to the project, you were in the background adding both libraries.
If you want to include kotlinx.coroutines on your projects, you’ll need to add:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
Note: The release is the 1.6.0. You should use this version with Kotlin 1.6.0 or 1.6.10. If you’re using a different one, open the release details section and confirm which version of the Kotlin compiler you should use.
There’s a set of limitations when targeting iOS: coroutines are single-threaded. This will be released with the new memory management model for Kotlin/Native that you’ll read in detail later in this chapter.
To overcome this, there’s a native-mt branch that supports multi-threading in iOS. This dependency is already in the project build.gradle.kts file from the shared module:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt") {
version {
strictly("1.6.0-native-mt")
}
}
Because Ktor uses the kotlinx.coroutines library in this case, it’s necessary to use the strictly function to force it to use this native-mt branch instead. Otherwise, you’ll get this error when running the iOS app:
kotlin.Error: Ktor native HttpClient requires kotlinx.coroutines version with
native-mtsuffix (like1.3.9-native-mt).
If for any reason you can’t use this native-mt version on your project, and you’re not using Ktor, you’ll need to create your own implementation of the Dispatchers.Main. Otherwise, you might have issues on your iOS app:
kotlin.IllegalStateException: There is no event loop. Use runBlocking { … } to start one.
This is because iOS only supports coroutines on the main thread. If you try to use the main dispatcher, it will fall back to Dispatchers.Default since it’s not supported on the main version.
Note: According to JetBrains, the
native-mtbranch won’t be available for kotlinx.coroutines 1.7.0 version and newer. It’s currently merged with the 1.6.0. Nevertheless, multithreading on iOS is only available with the new Kotlin/Native memory model.
It’s important to point out that although coroutines on iOS need to run on the main thread, this doesn’t mean that they will block it. There are two different types of operations:
- Blocking: When the thread stops, waiting for some operation. A quick example for this can be calling the sleep function.
- Suspending: The coroutine suspends, and the thread itself keeps running. This won’t block the thread, and other operations can still run during this state.
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.
Go to the domain directory inside shared/commonMain and create a new file: PlatformDispatcher.kt. Add:
internal expect val ioDispatcher: CoroutineContext
And import:
import kotlin.coroutines.CoroutineContext
You’re declaring it as ioDispatcher because the requirement of running coroutines on the main thread only exists for native. For the other platforms, you can run on the Default or IO thread pools.
Now, go to androidMain and create the domain package followed by the PlatformDispatcher.kt file with the actual implementation of ioDispatcher:
internal actual val ioDispatcher: CoroutineContext
get() = Dispatchers.IO
And import:
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
You can copy this folder and paste it inside the desktopMain directory at the same level as platform. The JVM supports the same version as Android, so you can use Dispatchers.Main to run your code in the UI-thread.
Now, navigate to iosMain and repeat the previous steps. Create the domain folder and the PlatformDispatcher.kt file, and this time, add:
package com.raywenderlich.learn.domain
internal actual val ioDispatcher: CoroutineContext
get() = IosMainDispatcher
Create a IosMainDispatcher.kt file inside the domain folder, and define the IosMainDispatcher object:
public object IosMainDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) { block.run() }
}
}
This is only possible because you have access to the Objective-C signatures from Multiplatform. The dispatch_async function that you’re calling is the one from the iOS platform.
Finally, import:
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
Now open the FeedPresenter.kt file from commonMain/presentation directory, and after the class declaration, add:
private val scope = CoroutineScope(ioDispatcher)
Note: If you’ve updated the
fetchMyGravatarfunction in the chapter above, you also need to replace theCoroutineScopecall withscope.
The CoroutineContext used in this case is going to be the ioDispatcher that you just defined. Replace all the calls to MainScope() with the scope variable that you created above.
Compile and run the application on the three platforms, and select one article from the list to read.
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 Dispatchers.Main or the ioDispatcher that you’ve created previously 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 library, in its latest versions, so you shouldn’t need to do anything from your side.
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")
}
This fetchImageUrlFromLink receives the link from an article and returns the page source code as the HttpResponse. It needs to be set as a suspend, so the current thread won’t block while it’s waiting for the server response.
Note: You need to set the
Acceptheader 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(result.bodyAsText())
//5
coroutineScope {
onSuccess(url)
}
} catch (e: Exception) {
coroutineScope {
onFailure(e)
}
}
}
Here’s a step-by-step breakdown of this logic:
-
invokeFetchImageUrlFromLinkis set assuspendsince it will call theFeedAPIto retrieve the page source code. - The
onSuccessandonFailurefunctions define how this function should behave, depending on if it was possible to retrieve an image for the article or not. - The
FeedAPIuses the Ktor HttpClient to make a network request. - 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.
- The
coroutineScopecreates a new coroutine, using its parent scope to run the functions ofonSuccessoronFailuredepending on whether the operation succeeded or not.
In the next sections, you’ll see different approaches to create and start a coroutine. Although both of them are valid, the API that they expose to the UI is different.
Note: A good rule of thumb for these cases is to decide between all the teams that are going to use the shared module what they feel most comfortable with. 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) {
scope.launch {
feed.invokeFetchImageUrlFromLink(
link,
onSuccess = { cb.onNewImageUrlAvailable(id, it, platform, null) },
onFailure = { cb.onNewImageUrlAvailable(id, "", platform, it) }
)
}
}
As you’ve read throughout this chapter, there are alternatives to implementing a coroutine. 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.
On androidApp and desktopApp, the changes are similar. On 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?) {
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
}
}
When this method receives a new url, the item to which it corresponds is updated. Updating the _items map automatically updates the UI.
Note:
viewModelScoperuns on the UI-thread.
Inside the withContext function of onNewDataAvailable, add:
_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 before.
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
}
Compile and run the apps for the three platforms and navigate to the latest screen.
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.
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 scope.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?>.
Note: The
scopeparameter is the variable created on “Implementing Dispatchers.Main for iOS”. If you skipped that section, you can useMainScopeinstead.
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(scope.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(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
fetchLinkImagefunction 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 includes 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
FeedHandlerImagethat 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 on 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 fetchFeedsWithPreview from RWEntryViewModel.swift in the iosApp root folder:
@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
}
}
}
}
Compile and run your app, and browse through the outstanding artwork of the raywenderlich.com articles. :]
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:
-
@ThreadLocal: Using this annotation in an object guarantees that it won’t be shared across other threads that try to access it. Instead, a new copy will be made which guarantees the object won’t freeze (in “Connecting to the API with Ktor”, from Chapter 12, “Networking”). -
Dispatcher.Main: There isn’t support for a coroutine to run directly in the UI-thread in iOS. To achieve this, you’ll need to implement your dispatcher at the platform-level as you read in “Implementing Dispatchers.Main for iOS”, from this chapter.
This new Kotlin/Native memory model aims to reduce the changes that you’ll have to do specifically for iOS.
Although it’s still in an experimental state, you can try it on your apps.
Enabling the new Kotlin/Native memory model
Learn is already using the latest libraries compatible with the new Kotlin/Native memory model:
-
kotlin-gradle-plugin: 1.6.10 -
ktor: 2.0.0-beta-1 -
coroutines-native-mt: 1.6.0
You still need to use the native-mt branch because the korio library was built using an older version of coroutines.
Open the gradle.properties file, located in the root folder, and add:
#Enable Kotlin/Native Memory Model
kotlin.native.binary.memoryModel=experimental
kotlin.native.binary.freezing=disabled
This activates the new memory model and disables freezing. You need to set this last attribute because not all libraries are fully compatible with the new model. If you don’t disable freezing, you’ll end up with InvalidMutabilityException or FreezingException on your iOS app.
To confirm that everything is working as expected, you can open the PlatformDispatcher.kt file in iosMain/domain in the shared module, and replace the getter with:
get() = Dispatchers.Default
There’s still no Dispatchers.IO for Native, but you can now use the Default instead of always using the main thread.
With this change, you need to update the calling function fetchFeeds from RWEntryViewModel on the iOS app:
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
}
}
}
Run your iOS app. Navigate through the app to confirm that everything is working as expected.
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.
Remember that you don’t need to run this logic sequentially — you can lunch 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
launchorasyncto 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.
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.