Kotlin Multiplatform. Practical multithreading (part 2)
Hi everyone! My name is Anna Zharkova, I’m Lead Mobile developer in “Usetech” software company.
In previous article I demontstrated one of the possible solutions to implement multitheading in Kotlin Multiplatform application. In this part I’m going to describe another solution, it will be KMM application with fully shared common code and common multithreading in shared business logic.
In previous samplewe used Ktor to make our common network client. This library makes all asynchronous work under its hood. In this case we have no need event to use DispatchQueue in our iOS native application. But in other cases we should use queues to request our business logic correctly and process the result responses. We also used MainScope to call suspended methods in our native Android app.
So if we want to implement multithreading in our shared code with common logic, we should use coroutines for all parts of our project, so we need to setup correct scopes and contexts of our coroutines.
Let’s begin with something simple. First of all, we will create our intemediate architectual component. I will use MVP pattern, so I need to make my presenters. This presenter will call all the methods of the specified service in its own CoroutineScope initialized with the CoroutineContext:
So, as previously mentioned, the presenter requests service methods in specified scope and then deliver the results to our UI:
We need to specify CoroutineDispatcher to initialize our CoroutineScope with CoroutineContext.
We need to use a platform-specific code, that’s why we’re going to customize it with expect/actual mechanism.
uiDispatcher should be used with UI-thread logic and other logic will be requested with defaultDispatcher.
It could be easily done in our androidMain, because it uses Kotlin JVM, so there are default dispatchers for both cases. Dispatchers.Default is a default dispatcher for Coroutines mechanism:
CoroutineDispatcher uses specified fabric MainDispatcherLoader under the hood to create MainCoroutineDispatcher for requesting platform:
Same mechanism used for DefaultDispatcher:
But not for all native platforms we can use existed default coroutine dispatchers. For example, such platforms as iOS work with KMM via Kotlin/Native, not Kotlin/JVM.
So if we try to use the same implementation, as we used for Android, we will receive an error:
Let’s take a look, what has happened.
GitHub Kotlin Coroutines Issue 470 contains information, that these special dispatchers for iOS haven’t been created in Kotlin/Native yet:
Issue 470 depends on Issue 462, so it is also not resolved:
Recommended solution for our case is to create our own dispatchers:
We have created MainDispatch with DispatchQueue.main and IODispatcher with DispatchQueue.global(). When we launch our code, we will get the same error.
The problem is, we cannot use dispatch_get_global_queue to dispatch our coroutines, because it is not bound to any particular thread in Kotlin/Native:
Secondly, Kotlin/Native doesn’t allow to move any mutable objects between threads. Included the coroutines.
So we can try to use MainDispatcher for all our cases:
But it is not enough. We also should freeze our objects before sharing the between threads. So we need to use freeze() command in this case:
But if we try to perform freeze() on already frozen object, FreezingException will be thrown. For example, all singletones are frozen by default.
That’s why we should use @ThreadLocal annotation to share singletones and @SharedImmutable for global variables:
We can simply use MainDispatcher for all our needs, when we use Ktor or another library that supports its own asynchronous processing. In common case for all long-running work we should use GlobalScope with the context of Dispatchers.Main/MainDispatcher:
So we can easily perform the switching between contexts to our service logic:
We created the common scope for all code performing in same suspended function. So everything will work correctly. It is not the only way to implement coroutine based logic, you can organize it with any approach you prefer.
You can also use wrapping blocks to share code with DispatchQueue.global():
Of course, you need to implement actual fun callFreeze(…) in androidMain, but you need just to put a callback into the completion block.
Finally, we got a completed application that works same way in both platforms:
Originally published at https://habr.com.