[Разработка мобильных приложений] Kotlin Multiplatform. Работаем с многопоточностью на практике. Ч.2
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Доброго всем времени суток! С вами я, Анна Жаркова, ведущий мобильный разработчик компании «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
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Разработка под iOS, IT-стандарты, Контекстная реклама, IT-компании] «Фонд электронных рубежей» раскритиковал кампанию Facebook против Apple
- [Разработка мобильных приложений] Kotlin Multiplatform. Работаем с многопоточностью на практике. Ч.1
- [Тестирование IT-систем, Разработка мобильных приложений, IT-инфраструктура, Разработка под Android, DevOps] VirtualBox — Запуск Android эмулятора в виртуальной среде для тестирования Android проекта
- [Разработка под iOS] Как мы делаем App Clips?
- [Программирование, Разработка под Android] Разрушаем мифы о производительности Android (перевод)
- [Open source, Разработка мобильных приложений, Flutter] Состояние Flutter на изолятах
- [Программирование, Разработка под iOS, Разработка мобильных приложений, Разработка под Android] Кошелёк Mobile Challenge: итоги конкурса и подробный разбор решений командой разработки
- [Браузеры] Браузер Vivaldi 2020 — Итоги года
- [Информационная безопасность, Разработка мобильных приложений] Пользователи системы «Помощник Москвы» смогут сообщать о незаконной торговле и скоплениях людей
- [Программирование, Разработка мобильных приложений, Dart, Flutter] Flutter под капотом: Owners
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_kotlin_multiplatform, #_ios, #_mnogopotochnost (многопоточность), #_kmm, #_android, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:26
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Доброго всем времени суток! С вами я, Анна Жаркова, ведущий мобильный разработчик компании «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 =========== Источник: habr.com =========== Похожие новости:
Разработка мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:26
Часовой пояс: UTC + 5