В предыдущей статье я рассказывала про один из способов реализации многопоточности в приложении Kotlin Multiplatform. Сегодня мы рассмотрим альтернативную ситуацию, когда мы реализуем приложение с максимально расшариваемым общим кодом, перенося всю работу с потоками в общую логику.
В прошлом примере нам помогла библиотека Ktor, которая взяла на себя всю основную работу по обеспечению асинхронности в сетевом клиенте. Это избавило нас от необходимости использовать DispatchQueue на iOS в том конкретном случае, но в других нам бы пришлось использовать задание очереди исполнения для вызова бизнес-логики и обработки ответа. На стороне Android мы использовали MainScope для вызова suspended функции.
Итак, если мы хотим реализовать единообразную работу с многопоточностью в общем проекте, то нам потребуется корректно настроить scope и контекст корутины, в котором она будет выполняться.
Начнем с простого. Создадим нашего архитектурного посредника, который будет вызывать методы сервиса в своем scope, получаемом из контекста корутины:
class PresenterCoroutineScope(context: CoroutineContext) : CoroutineScope { private var onViewDetachJob = Job() override val coroutineContext: CoroutineContext = context + onViewDetachJob fun viewDetached() { onViewDetachJob.cancel() } } //Базовый класс для посредника abstract class BasePresenter(private val coroutineContext: CoroutineContext) { protected var view: T? = null protected lateinit var scope: PresenterCoroutineScope fun attachView(view: T) { scope = PresenterCoroutineScope(coroutineContext) this.view = view onViewAttached(view) } }
Вызываем сервис в методе посредника и передаем нашему UI:
class MoviesPresenter:BasePresenter(defaultDispatcher){ var view: IMoviesListView? = null fun loadData() { //запускаем в скоупе scope.launch { service.getMoviesList{ val result = it if (result.errorResponse == null) { data = arrayListOf() data.addAll(result.content?.articles ?: arrayListOf()) withContext(uiDispatcher){ view?.setupItems(data) } } } } //IMoviesListView - интерфейс/протокол, который будут реализовывать UIViewController и Activity. interface IMoviesListView { fun setupItems(items: List<MovieItem>) } class MoviesVC: UIViewController, IMoviesListView { private lazy var presenter: IMoviesPresenter? = { let presenter = MoviesPresenter() presenter.attachView(view: self) return presenter }() override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) presenter?.attachView(view: self) self.loadMovies() } func loadMovies() { self.presenter?.loadMovies() } func setupItems(items: List<MovieItem>){} //.... class MainActivity : AppCompatActivity(), IMoviesListView { val presenter: IMoviesPresenter = MoviesPresenter() override fun onResume() { super.onResume() presenter.attachView(this) presenter.loadMovies() } fun setupItems(items: List<MovieItem>){} //...
Чтобы корректно создавать scope из контекста корутины, нам потребуется задать диспетчер корутины.
Это платформозависимая логика, поэтому используем кастомизацию с помощью expect/actual.
expect val defaultDispatcher: CoroutineContext expect val uiDispatcher: CoroutineContext
uiDispatcher будет отвечать за работу в потоке UI. defaultDispatcher будем использовать для работы вне UI потока.
Проще всего создать в androidMain, т.к в Kotlin JVM есть готовые реализации для диспетчеров корутин. Для доступа к соответствующим потокам используем CoroutineDispatchers Main (UI поток) и Default (стандартный для Coroutine):
actual val uiDispatcher: CoroutineContext get() = Dispatchers.Main actual val defaultDispatcher: CoroutineContext get() = Dispatchers.Default
Диспетчер MainDispatcher выбирается для платформы под капотом CoroutineDispatcher с помощью фабрики диспетчеров MainDispatcherLoader:
internal object MainDispatcherLoader { private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true) @JvmField val dispatcher: MainCoroutineDispatcher = loadMainDispatcher() private fun loadMainDispatcher(): MainCoroutineDispatcher { return try { val factories = if (FAST_SERVICE_LOADER_ENABLED) { FastServiceLoader.loadMainDispatcherFactory() } else { // We are explicitly using the // `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()` // form of the ServiceLoader call to enable R8 optimization when compiled on Android. ServiceLoader.load( MainDispatcherFactory::class.java, MainDispatcherFactory::class.java.classLoader ).iterator().asSequence().toList() } @Suppress("ConstantConditionIf") factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories) ?: createMissingDispatcher() } catch (e: Throwable) { // Service loader can throw an exception as well createMissingDispatcher(e) } } }
Так же и с Default:
internal object DefaultScheduler : ExperimentalCoroutineDispatcher() { val IO: CoroutineDispatcher = LimitingDispatcher( this, systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)), "Dispatchers.IO", TASK_PROBABLY_BLOCKING ) override fun close() { throw UnsupportedOperationException("$DEFAULT_DISPATCHER_NAME cannot be closed") } override fun toString(): String = DEFAULT_DISPATCHER_NAME @InternalCoroutinesApi @Suppress("UNUSED") public fun toDebugString(): String = super.toString() }
Однако, не для всех платформ есть реализации диспетчеров корутин. Например, для iOS, который работает с Kotlin/Native, а не с Kotlin/JVM.
Если мы попробуем использовать код, как в Android, то получим ошибку:

Давайте разберем, в чем же у нас дело.
Issue 470 c GitHub Kotlin Coroutines содержит информацию, что специальные диспетчеры еще не реализованы для iOS:

Issue 462, от которой зависит 470, то же еще в статусе Open:

Рекомендуемым решением является создание собственных диспетчеров для iOS:
actual val defaultDispatcher: CoroutineContext get() = IODispatcher actual val uiDispatcher: CoroutineContext get() = MainDispatcher private object MainDispatcher: CoroutineDispatcher(){ override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatch_get_main_queue()) { try { block.run() }catch (err: Throwable) { throw err } } } } private object IODispatcher: CoroutineDispatcher(){ override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0.toULong())) { try { block.run() }catch (err: Throwable) { throw err } } }
При запуске мы получим ту же самую ошибку.
Во-первых, мы не можем использовать dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(),0.toULong())), потому что он не привязан ни к одному потоку в Kotlin/Native:

Во-вторых, Kotlin/Native в отличие от Kotlin/JVM не может шарить корутины между потоками. А также любые изменяемые объекты.
Поэтому мы используем MainDispatcher в обоих случаях:
actual val ioDispatcher: CoroutineContext get() = MainDispatcher actual val uiDispatcher: CoroutineContext get() = MainDispatcher @ThreadLocal private object MainDispatcher: CoroutineDispatcher(){ override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatch_get_main_queue()) { try { block.run().freeze() }catch (err: Throwable) { throw err } } }
Для того, чтобы мы могли передавать изменяемые блоки кода и объекты между потоками, нам нужно их замораживать перед передачей с помощью команды freeze():

Однако, если мы попытаемся заморозить уже замороженный объект, например, синглтоны, которые считаются замороженными по умолчанию, то получим FreezingException.
Чтобы этого не произошло, помечаем синглтоны аннотацией @ThreadLocal, а глобальные переменные @SharedImmutable:
/** * Marks a top level property with a backing field or an object as thread local. * The object remains mutable and it is possible to change its state, * but every thread will have a distinct copy of this object, * so changes in one thread are not reflected in another. * * The annotation has effect only in Kotlin/Native platform. * * PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES. */ @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) @Retention(AnnotationRetention.BINARY) public actual annotation class ThreadLocal /** * Marks a top level property with a backing field as immutable. * It is possible to share the value of such property between multiple threads, but it becomes deeply frozen, * so no changes can be made to its state or the state of objects it refers to. * * The annotation has effect only in Kotlin/Native platform. * * PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES. */ @Target(AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.BINARY) public actual annotation class SharedImmutable
В итоге, внеся все правки, мы получаем работающее одинаково на обеих платформах приложение:

Исходники примера github.com/anioutkazharkova/movies_kmp
tproger.ru/articles/creating-an-app-for-kotlin-multiplatform
github.com/JetBrains/kotlin-native
github.com/JetBrains/kotlin-native/blob/master/IMMUTABILITY.md
github.com/Kotlin/kotlinx.coroutines/issues/462
helw.net/2020/04/16/multithreading-in-kotlin-multiplatform-apps
ссылка на оригинал статьи https://habr.com/ru/post/533952/
Добавить комментарий