Kotlin Multiplatform. Работаем с многопоточностью на практике. Ч.2

от автора

Доброго всем времени суток! С вами я, Анна Жаркова, ведущий мобильный разработчик компании «Usetech».
В предыдущей статье я рассказывала про один из способов реализации многопоточности в приложении 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *