По мере того, как приложение становится все более сложным, параллелизм становится фундаментальной темой, которую вам необходимо рассмотреть. Learn делает несколько запросов к сети — это нужно делать асинхронно, чтобы гарантировать, что они не повлияют на пользовательский интерфейс.
Примечание : Эта глава следует за проектом, который вы начали в Главе 12, «Сеть». Или вы можете использовать начальный проект этой главы.
Вот что вы будете делать в этой главе:
- Вы узнаете, что такое сопрограммы и как их реализовать.
- Вы включите новую модель памяти Kotlin/Native.
Необходимость структурированного параллелизма
Структурированный параллелизм позволяет выполнять несколько вычислений вне потока пользовательского интерфейса, чтобы приложение оставалось максимально отзывчивым. Он отличается от параллелизма в том смысле, что задача может выполняться только в пределах своей родительской задачи, которая не может завершиться раньше всех своих дочерних задач.
![[Снимок экрана 2022-03-17 в 20.31.59.png]]
UI-threadsecondary threadtask#1#2#3segmentsstructured concurrencyscreen refresh ratecoroutine #1coroutine #2coroutine #3
Рис. 13.1. Диаграмма, показывающая несколько задач, выполняющихся в структурированном параллелизме.
Для повышения производительности эти три задачи выполняются в одном потоке, но одновременно друг с другом. Они были разделены на более мелкие сегменты, которые работали независимо.
Структурированный параллелизм недавно приобрел большую популярность с выпуском kotlinx.coroutines для Android и async/await для iOS — в основном из-за того, насколько легко теперь выполнять асинхронные операции.
Различные решения для параллелизма
Для Kotlin Multiplatform создан набор библиотек, поддерживающих параллелизм:
- kotlinx.coroutines : самый популярный, в основном из-за его использования разработчиками Android и рекомендаций от JetBrains и Google. Он легкий, позволяет запускать несколько сопрограмм в одном потоке и поддерживает обработку и отмену исключений.
- Reaktive : реализация реактивных расширений с использованиемшаблона Observable .
- CoroutineWorker : поддерживает многопоточные сопрограммы.
В этой главе вы узнаете, как использовать kotlinx.coroutines . Спойлер: вы уже работали с сопрограммами раньше. :]
Если вы уже знакомы с сопрограммами, вы можете пропустить несколько следующих разделов и перейти к разделу «Структурированный параллелизм в iOS» или сразу к разделу «Работа с kotlinx.coroutines», чтобы узнать о следующих разработках в Learn .
Понимание kotlinx.coroutines
Ktor использует сопрограммы для выполнения сетевых запросов, не блокируя UI-поток, так что вы уже невольно использовали их в предыдущей главе.
Откройте файл FeedPresenter.kt из папки shared/commonMain/presentation и найдите fetchAllFeeds
и fetchFeed
. В первой функции у вас есть:
for (feed in content) { fetchFeed(feed.platform, feed.url) }
Если бы вы не использовали сопрограммы на fetchFeed
, эти инструкции выполнялись бы последовательно. Другими словами, приложение будет переходить к следующему элементу только после fetchFeed
возврата, что приведет к задержке запуска приложения.
Приостановить функции
Функции приостановки лежат в основе сопрограмм. Как следует из названия, они позволяют вам приостановить сопрограмму и возобновить ее позже, не блокируя основной поток.
Сетевые запросы являются одним из вариантов использования функций приостановки . Откройте файл FeedAPI.kt в папке shared/commonMain/data и посмотрите на объявление функций:
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()
Это все функции приостановки . Поскольку ответ может занять некоторое время, приложение не может заблокировать и дождаться возврата какой-либо из этих функций.
Это изображение определяет поток, который вызывается триггером fetchRWEntry
.
UI-threadnew coroutinefetchFeed()MainScope().launch { //create coroutine }suspend fun// deserializing// notifying the UIsuspend funsuspendswaiting for server responseresumesrequesting RW feeds123546fetchFeed()fetchFeed()FetchRWEntry()invokeFetchRWEntry()invokeFetchRWEntry()
Рис. 13.2 — Диаграмма, показывающая различные этапы сетевого запроса с сопрограммами.
Точкой входа для всех платформ является fetchAllFeeds
функция из файла shared/commonMain/presentation/FeedPresenter.kt . После вызова он перебирает все RSS-каналы и вызывает fetchFeed
каждый из своих URL-адресов:
- Это тяжелая операция, которая может заблокировать пользовательский интерфейс. Чтобы избежать этого, вы будете делать это асинхронно. Создайте сопрограмму, вызвав
launch
. - После запуска он вызывает
invokeFetchRWEntry
из shared/commonMain/domain/GetFeedData.kt . Функция приостановки вызывает FeedAPI для выполнения запроса. - Эта функция приостанавливается после выполнения запроса и ожидает, пока не будет получен ответ или не истечет время ожидания соединения.
- Это делается в отдельном потоке, поэтому пользовательский интерфейс не блокируется.
- После получения ответа
fetchRWEntry
возобновляется и возвращается кinvokeFetchRWEntry
, который теперь может десериализовать полученную информацию. - Когда этот процесс завершается, выполняются функции
onSuccess
илиonFailure
— в зависимости от результата — и пользовательский интерфейс получает обновление. Поскольку вы используете сопрограммуMainScope
,launch
это означает, что она будет работать в потоке пользовательского интерфейса. Вы увидите это подробно в следующем разделе.
Ключевым моментом является то, что вы можете вызывать функцию приостановки только из другой или внутри сопрограммы.
Область действия и контекст сопрограммы
Вернитесь на FeedPresenter.kt из shared/commonMain/presentation и найдите fetchFeed
функцию:
private fun fetchFeed(platform: PLATFORM, feedUrl: String, cb: FeedData) { MainScope().launch { // Call to invokeFetchRWEntry } }
Вы уже знаете, что launch
создает новую сопрограмму, но что такое MainScope
? Область сопрограммы — это место, где будет выполняться сопрограмма — в данном случае это будет основной поток.
Если вы откроете исходный код MainScope
:
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Вы можете видеть, что ContextScope
он построен с использованием:
SupervisorJob
: когда вы создаете сопрограмму, она возвращает значениеJob
, соответствующее ее экземпляру. Это позволяет вам отменить или узнать больше о его текущем состоянии:isActive
: если он запущен в данный момент.isCompleted
: Если вся его работа, а также его дети закончились. Более того, когда текущее задание будет отменено или завершится ошибкой, его значение будет истинным.isCancelled
: при отмене или сбое текущего задания.
Разница между a SupervisorJob
и a Job
заключается в том, что у них разные политики. В первом дочерние элементы ведут себя независимо — если один выйдет из строя, другие не пострадают, тогда как во втором, если родитель выйдет из строя, все его дочерние элементы будут отменены.
Dispatchers
: определить, в каком потоке должна выполняться сопрограмма:Default
: использует общий пул потоков.Main
: имеет разное поведение в зависимости от платформы, которая работает в данный момент. В JVM и AndroidMain
соответствует потоку пользовательского интерфейса и должен использоваться только для операций, обновляющих пользовательский интерфейс. На Native это то же самое, что иDefault
диспетчер.Unconfined
: не имеет связанной политики многопоточности. Не переключается на какой-либо конкретный поток.IO
: следует использовать для длительных и тяжелых задач, поскольку это один общий пул потоков, оптимизированный для таких типов операций. В настоящее время он недоступен для iOS.
Когда вы создаете сопрограмму, вы должны определить диспетчер , в котором она должна работать, но вы всегда можете переключить контекст позже при ее выполнении, вызвав withContext
с добавленным Dispatcher
в качестве аргумента.
В fetchMyGravatar
функции из FeedPresenter.kt вы запускаете сопрограмму в основном потоке, хотя единственная часть, которую необходимо запустить, — это вызовы onSuccess
и . onFailure
Вы можете обновить существующую функцию, чтобы использовать поток по умолчанию для сетевых запросов, а когда данные доступны, переключиться на основной поток, чтобы можно было обновить пользовательский интерфейс:
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) } } }
Вот что вы делаете:
- Создает новую сопрограмму в потоке из пула потоков по умолчанию и запускает ее. В идеале следует использовать диспетчер ввода -вывода. Однако он не поддерживается в iOS, поэтому вам потребуется создать для него логику для конкретной платформы, как вы увидите в разделе «Реализация диспетчеров: ввод-вывод для iOS».
invokeGetMyGravatar
является функцией приостановки. Когда есть запрос, он приостанавливается до тех пор, пока не будет ответа сервера. Как только это произойдет, сопрограмма возобновится.- Пользовательский интерфейс может обновляться только из UI-потока, поэтому необходимо переключиться с
Default
диспетчера наMain
тот. Это можно сделать только из сопрограммы. onMyGravatarData
теперь вызывается из UI-потока, поэтому пользователь может видеть эти вновь полученные данные.
Вам также потребуется обновить invokeGetMyGravatar
функцию, чтобы она вместо этого возвращала результат. Откройте файл GetFeedData.kt из commonMain/domain и измените его на:
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() } }
Помимо MainScope
, у вас также есть GlobalScope
. Как правило, он используется в сценариях, где сопрограмма должна работать на протяжении всего выполнения приложения.
Вы должны быть особенно осторожны при использовании этой функции. Если сопрограмма не может завершиться, она будет продолжать использовать ресурсы, возможно, до тех пор, пока пользователь не закроет приложение.
Если вам нужно обновить пользовательский интерфейс и вы используете GlobalScope
, вы должны переключиться на поток пользовательского интерфейса раньше. В противном случае при запуске приложения для iOS вы получите следующее исключение:
kotlin.native.IncorrectDereferenceException: незаконная попытка доступа к неразделяемому (…) из другого потока
У вас также есть coroutineScope
функция, которая позволяет вам создать сопрограмму, но она использует родительскую область в качестве контекста. Он имеет некоторые особенности, а именно:
- Если родитель отменяется, он отменяет все его дочерние элементы.
- Только после того, как закончатся все дочерние элементы, родитель также может завершиться.
Строители корутин, объем и контекст
Вы видели, как запустить сопрограмму, вызвав launch
. Эта функция является частью сборщиков сопрограмм :
runBlocking
: блокирует текущий поток до тех пор, пока созданная им сопрограмма не завершится.
Примечание . Его не следует использовать внутри существующей сопрограммы, так как это остановит ее выполнение.
launch
: Создает сопрограмму, не блокируя текущий поток. Вы можете определить CoroutineScope , откуда он должен запускаться. Эта область действия гарантирует параллелизм структуры — другими словами, сопрограмма завершается только после того, как все ее дочерние элементы завершили свои операции.async
: Похож наlaunch
то, как он построен и как он работает. Он отличается типом возвращаемого значения тем, что в данном случае это не Job , а объект Deferred , который будет содержать будущий результат этой функции.
Вернитесь к fetchMyGravatar
функции и добавьте ниже:
private suspend fun fetchMyGravatar(): GravatarEntry { return CoroutineScope(Dispatchers.Default).async { feed.invokeGetMyGravatar( hash = md5(GRAVATAR_EMAIL) ) }.await() }
fetchMyGravatar
теперь suspend
функция. При таком подходе вам не нужны обратные вызовы onSuccess
и onFailure
для обновления пользовательского интерфейса, поскольку вы собираетесь вернуть файл GravatarEntry
. Вам нужно позвонить await
в конце, чтобы вернуть его окончательное значение вместо Deferred .
Стоит отметить, что эта функция аналогична использованию withContext
:
private suspend fun fetchMyGravatar(): GravatarEntry { return withContext(CoroutineScope(Dispatchers.Default).coroutineContext) { feed.invokeGetMyGravatar( hash = md5(GRAVATAR_EMAIL) ) } }
Основное различие между обоими вызовами заключается в том, что CoroutineScope
они не используют ту же область действия, что и вызывающий объект.
Следование этому подходу означает, что вам также придется сделать еще несколько обновлений. Чтобы использовать ту же логику для уведомления пользовательского интерфейса с помощью обратных вызовов, вам нужно изменить fetchMyGravatar(cb: FeedData)
на:
public fun fetchMyGravatar(cb: FeedData) { Logger.d(TAG, "fetchMyGravatar") CoroutineScope(Dispatchers.Default).launch { cb.onMyGravatarData(fetchMyGravatar()) } }
В противном случае вы можете вернуть GravatarEntry
прямо в пользовательский интерфейс. Вы увидите, как реализовать этот второй подход, в разделе «Создание сопрограммы с асинхронным управлением».
С этим изменением вам необходимо обновить вызывающую функцию fetchProfile
из RWEntryViewModel в приложении iOS, чтобы можно было успешно обновить пользовательский интерфейс:
func fetchProfile() { FeedClient.shared.fetchProfile { profile in Logger().d(tag: TAG, message: "fetchProfile: \(profile)") DispatchQueue.main.async { self.profile = profile } } }
Вам не нужно обновлять приложение для Android или настольное приложение, поскольку viewModelScope работает в потоке пользовательского интерфейса.
Примечание . В следующих разделах вы узнаете, что iOS по умолчанию является однопоточной. Только когда вы включите новую модель памяти Kotlin/Native, вы сможете использовать многопоточность. При этом, если вы хотите скомпилировать свое приложение сейчас, вам нужно заменить его
Dispatchers.Default
наDispatchers.Main
. Кроме того, вы можете реализовать диспетчер на уровне конкретной платформы, как вы увидите в разделе «Реализация диспетчеров: ввод-вывод для iOS».
Отмена сопрограммы
Хотя вы не собираетесь использовать его в Learn , стоит упомянуть, что вы можете отменить сопрограмму, вызвав cancel()
объект Job , возвращаемый launch
.
Если вы используете async
, вам придется реализовать решение, подобное этому:
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()
Когда вы отменяете сопрограмму, автоматически CancellationException
выбрасывается a. Вы можете поймать его для реализации определенного поведения, которое может понадобиться вашему приложению, или для очистки ресурсов.
Структурированный параллелизм в iOS
У Apple есть похожее решение для структурированного параллелизма: async/await .
Примечание . async/await доступен только в том случае, если вы используете Xcode 13.2 или более позднюю версию и запускаете свое приложение на iOS 13 или более поздних версиях.
С async/await вам больше не нужно использовать обработчики завершения. Вместо этого вы можете использовать async
ключевое слово после объявления функции. Если вы хотите дождаться его возврата, добавьте await
перед вызовом функции приостановки :
private func fetchMyGravatar() async -> GravatarEntry { return await feed.invokeGetMyGravatar( hash = md5(GRAVATAR_EMAIL) ) }
Что похоже на Kotlin:
private suspend fun fetchMyGravatar(): GravatarEntry { return withContext(Dispatchers.IO) { feed.invokeGetMyGravatar( hash = md5(GRAVATAR_EMAIL) ) } }
Следуя той же логике , что и функции приостановки , вы можете вызывать асинхронную функцию только из другой или из асинхронной задачи. В Kotlin это соответствует вызову функции из сопрограммы.
Свифт использует Task
. Используя Task
, предыдущий пример можно преобразовать в:
private func fetchMyGravatar() { Task { let profile = await feed.invokeGetMyGravatar( hash = md5(GRAVATAR_EMAIL) ) await profile } }
С kotlinx.coroutines это:
private suspend fun fetchMyGravatar() = { CoroutineScope(Dispatchers.IO).launch { async { feed.invokeGetMyGravatar( hash = md5(GRAVATAR_EMAIL) ) }.await } }
Использование kotlinx.coroutines
Пришло время обновить учиться . В предыдущей главе вы узнали, как реализовать сетевой уровень в мультиплатформе. Для этого вы добавили библиотеку Ktor и написали логику для получения RSS-канала raywenderlich.com и анализа его ответов, которые впоследствии обновляют пользовательский интерфейс.
Однако для этого раздела осталась небольшая деталь: Ktor собирается с использованием kotlinx.coroutines . Вот почему функции MainScope
, launch
и suspend
кажутся знакомыми в разделе «Понимание kotlinx.coroutines».
Добавление kotlinx.coroutines в конфигурацию Gradle
Поскольку Ktor включает kotlinx.coroutines , когда вы добавляли эту библиотеку в проект, вы добавляли обе библиотеки в фоновом режиме.
Если вы хотите включить kotlinx.coroutines в свои проекты, вам нужно добавить:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
Примечание . Версия 1.6.0. Вы должны использовать эту версию с Kotlin 1.6.0 или 1.6.10. Если вы используете другой, откройте раздел сведений о выпуске и подтвердите, какую версию компилятора Kotlin следует использовать.
При ориентации на iOS существует ряд ограничений : сопрограммы являются однопоточными. Он будет выпущен с новой моделью управления памятью для Kotlin/Native, о которой вы подробно прочтете позже в этой главе.
Чтобы решить эту проблему, существует native-mt
ветка, поддерживающая многопоточность в iOS. Эта зависимость уже есть в файле проекта build.gradle.kts из общего модуля:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt") { version { strictly("1.6.0-native-mt") } }
Поскольку в этом случае Ktor использует библиотеку kotlinx.coroutines , необходимо использовать strictly
функцию, чтобы заставить его использовать эту native-mt
ветку вместо этого. В противном случае вы получите эту ошибку при запуске приложения iOS:
kotlin.Error: Ktor native HttpClient требует версию kotlinx.coroutines с
native-mt
суффиксом (например1.3.9-native-mt
).
Если по какой-либо причине вы не можете использовать эту native-mt
версию в своем проекте и не используете Ktor, вам необходимо создать собственную реализацию Dispatchers.Main . В противном случае у вас могут возникнуть проблемы с приложением для iOS:
kotlin.IllegalStateException: цикла событий нет. Используйте runBlocking {…}, чтобы запустить его.
Это связано с тем, что iOS поддерживает сопрограммы только в основном потоке. Если вы попытаетесь использовать основной диспетчер, он вернется к Dispatchers.Default , поскольку он не поддерживается в основной версии.
Примечание . Согласно JetBrains ,
native-mt
ветка не будет доступна для версии kotlinx.coroutines 1.7.0 и новее. В настоящее время он объединен с 1.6.0. Тем не менее, многопоточность на iOS доступна только с новой моделью памяти Kotlin/Native.
Важно отметить, что хотя сопрограммы в iOS должны выполняться в основном потоке, это не означает, что они будут его блокировать. Различают два типа операций:
- Блокировка : когда поток останавливается, ожидая какой-либо операции. Быстрым примером этого может быть вызов функции сна .
- Suspending : сопрограмма приостанавливается, а сам поток продолжает работать. Это не блокирует поток, и в этом состоянии могут выполняться другие операции.
Внедрение диспетчеров: ввод-вывод для iOS
Хотя без новой модели памяти Kotlin/Native iOS является однопоточной, это не означает, что вы не можете использовать преимущества многопоточности на других платформах. Однако вам потребуется реализовать эту поддержку.
Перейдите в каталог домена внутри shared/commonMain и создайте новый файл: PlatformDispatcher.kt . Добавлять:
internal expect val ioDispatcher: CoroutineContext
И импорт:
import kotlin.coroutines.CoroutineContext
Вы объявляете это так, ioDispatcher
потому что требование запуска сопрограмм в основном потоке существует только для нативных. Для других платформ вы можете работать с пулами потоков Default или IO .
Теперь перейдите в androidMain и создайте пакет домена , а затем файл PlatformDispatcher.kt с actual
реализацией ioDispatcher
:
internal actual val ioDispatcher: CoroutineContext get() = Dispatchers.IO
И импорт:
import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext
Вы можете скопировать эту папку и вставить ее в каталог desktopMain на том же уровне, что и platform . JVM поддерживает ту же версию, что и Android, поэтому вы можете использовать Dispatchers.Main
ее для запуска своего кода в UI-потоке.
Теперь перейдите к iosMain и повторите предыдущие шаги. Создайте папку домена и файл PlatformDispatcher.kt и на этот раз добавьте:
package com.raywenderlich.learn.domain internal actual val ioDispatcher: CoroutineContext get() = IosMainDispatcher
Создайте файл IosMainDispatcher.kt внутри папки домена и определите IosMainDispatcher
объект:
public object IosMainDispatcher : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatch_get_main_queue()) { block.run() } } }
Это возможно только потому, что у вас есть доступ к подписям Objective-C из мультиплатформы. Функция dispatch_async
, которую вы вызываете, относится к платформе iOS.
Наконец, импортируйте:
import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Runnable import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue
Теперь откройте файл FeedPresenter.kt из каталога commonMain/presentation и после объявления класса добавьте:
private val scope = CoroutineScope(ioDispatcher)
Примечание . Если вы обновили
fetchMyGravatar
функцию в главе выше, вам также необходимо заменитьCoroutineScope
вызов наscope
.
CoroutineContext , используемый в этом случае, будет тем ioDispatcher
, который вы только что определили. Замените все вызовы MainScope()
на scope
переменную, которую вы создали выше.
Скомпилируйте и запустите приложение на трех платформах и выберите одну статью из списка для чтения.

Рис. 13.3 – Лента в приложении для Android

Рис. 13.4 – Лента в приложении для iOS

Рис. 13.5 — Лента в настольном приложении
Устранение неполадок kotlinx.coroutines в iOS
Продолжая свое путешествие с мультиплатформой вне этой книги, вы, вероятно, обнаружите эту ошибку:
Неперехваченное исключение Kotlin: kotlin.native.concurrent.InvalidMutabilityException: попытка мутации заморожена
Это InvalidMutabilityException
означает, что вы обращаетесь к объекту, принадлежащему другому потоку, что в настоящее время невозможно. Подтвердите, используете ли вы Dispatchers.Main или ioDispatcher , который вы создали ранее, для доступа к этому объекту.
Если вы все еще сталкиваетесь с проблемами:
- Удалите папку сборки в корневом каталоге проекта.
- Удалите папку сборки в общем каталоге в корневом каталоге проекта.
Замороженное состояние
В некоторых случаях вам может потребоваться заморозить объекты при запуске приложения для iOS, чтобы избежать упомянутой выше ошибки. После freeze()
вызова над объектом он становится неизменяемым. Другими словами, его нельзя изменить — это позволяет использовать его в разных потоках.
Еще одним преимуществом использования библиотеки kotlinx.coroutines является то, что эта логика уже встроена в библиотеку, в ее последних версиях, поэтому вам не нужно ничего делать с вашей стороны.
Работа с kotlinx.coroutines
В приложении перейдите на последний экран. Вы увидите пару статей, сгруппированных по разным разделам, которые вы можете открыть и пролистать, но ни одна из них не имеет изображения. Пришло время изменить это!
Создание функции приостановки
Начните с открытия файла FeedAPI.kt из data/commonMain в общем модуле. После fetchRWEntry
добавь:
public suspend fun fetchImageUrlFromLink(link: String): HttpResponse = client.get(link) { header(HttpHeaders.Accept, "text/html") }
Он fetchImageUrlFromLink
получает ссылку из статьи и возвращает исходный код страницы в виде файла HttpResponse
. Он должен быть установлен как suspend
, чтобы текущий поток не блокировался, пока он ожидает ответа сервера.
Примечание . Вам необходимо установить
Accept
заголовок в этом запросе, иначе сервер вернет ошибку 406, что неприемлемо.
Затем откройте файл GetFeedData.kt из папки shared/commonMain/domain и добавьте в класс следующий метод:
//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) } } }
Вот пошаговая разбивка этой логики:
invokeFetchImageUrlFromLink
установлен какsuspend
поскольку он будет вызыватьFeedAPI
для получения исходного кода страницы.- Функции
onSuccess
иonFailure
определяют поведение этой функции в зависимости от того, удалось ли получить изображение для статьи или нет. - Использует
FeedAPI
Ktor HttpClient для выполнения сетевого запроса. - Поскольку не существует API для получения URL-адреса изображения, вы будете анализировать HTML-код и искать определенный тег изображения. Наряду с сетевым запросом это будет тяжелой задачей. Итак, эту логику нужно вызывать из сопрограммы.
- Создает
coroutineScope
новую сопрограмму, используя ее родительскую область для запуска функцийonSuccess
илиonFailure
в зависимости от того, была ли операция успешной или нет.
В следующих разделах вы увидите различные подходы к созданию и запуску сопрограммы. Хотя оба они допустимы, API, которые они предоставляют пользовательскому интерфейсу, отличаются.
Примечание . Хорошее эмпирическое правило для таких случаев — решить между всеми командами, которые собираются использовать общий модуль, что им удобнее. Это особенно важно для iOS-программистов, которые плохо знакомы с Kotlin и могут чувствовать себя перегруженными адаптацией к новому языку. Взаимодействие с вашим общим модулем должно быть таким же, как и с любой другой библиотекой, существующей для iOS.
Создание сопрограммы с запуском
Теперь, когда вы реализовали функции для запроса и разбора данных, вам осталось только создать сопрограмму, а ее… запустить. :]
Откройте файл FeedPresenter.kt внутри commonMain/presentation . В общем модуле и перед fetchMyGravatar(cb: FeedData)
функцией добавьте:
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) } ) } }
Как вы читали в этой главе, существуют альтернативы реализации сопрограммы. В этом подходе вы используете FeedData
прослушиватель, определенный на уровне пользовательского интерфейса. После invokeFetchImageUrlFrom
завершения он либо вызовет функции, onSuccess
либо onFailure
которые, в свою очередь, вызовут onNewImageUrlAvailable
обратный вызов в пользовательском интерфейсе с новыми полученными данными или с исключением в случае ошибки.
Теперь подключите пользовательский интерфейс вашего приложения к этой новой функции.
В androidApp и desktopApp изменения аналогичны. В обоих проектах перейдите в ui/home , откройте файл FeedViewModel.kt и обновите onNewImageUrlAvailable
обратный вызов с помощью:
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 } }
Когда этот метод получает новое url
значение, элемент, которому он соответствует, обновляется. Обновление _items
карты автоматически обновляет пользовательский интерфейс.
Примечание :
viewModelScope
выполняется в потоке пользовательского интерфейса.
Внутри withContext
функции onNewDataAvailable
добавьте:
_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) }
Теперь, когда приложение получает новые статьи, оно автоматически запрашивает их изображения.
Создайте fetchLinkImage
функцию:
private fun fetchLinkImage(platform: PLATFORM, id: String, link: String) { Logger.d(TAG, "fetchLinkImage | link=$link") presenter.fetchLinkImage(platform, id, link, this) }
fetchLinkImage
вызывает fetchLinkImage
из созданного ранее файла FeedPresenter.kt .
В iosApp откройте файл FeedClient.swift , который находится в каталоге расширений, и найдите fetchLinkImage
. Чтобы также вызывать fetchLinkImage
из класса FeedPresenter.kt , измените эту функцию на:
public func fetchLinkImage(_ platform: PLATFORM, _ id: String, _ link: String, completion: @escaping FeedHandlerImage) { feedPresenter.fetchLinkImage(platform: platform, id: id, link: link, cb: self) handlerImage = completion }
Скомпилируйте и запустите приложения для трех платформ и перейдите к последнему экрану.

Рис. 13.6 — Android-приложение: просмотр последних статей

Рис. 13.7 — Приложение для ПК: просмотр последних статей

Рис. 13.8 — Приложение для iOS: просмотр последних статей
Создание корутины с асинхронностью
В качестве альтернативы предыдущему подходу, когда вы используете обратные вызовы для уведомления пользовательского интерфейса о появлении новых данных, вы можете приостановить fetchLinkImage
функцию до тех пор, пока не будет получен окончательный результат. Для этого вам нужно использовать async
вместо launch
.
Вернитесь к файлу FeedPresenter.kt в commonMain/presentation в общем модуле и обновите функцию fetchLinkImage
:
public suspend fun fetchLinkImage(link: String): String { return scope.async { feed.invokeFetchImageUrlFromLink( link ) }.await() }
Как видите, параметры и больше не нужны platform
, id
так как вы собираетесь вернуть URL-адрес изображения, если оно существует. Функция async
позволяет вернуть объект, await
ожидая готовности ответа. Вместо возврата Deferred — в данном случае это будет Deferred .
Примечание . Параметр
scope
— это переменная, созданная в разделе «Реализация Dispatchers.Main для iOS». Если вы пропустили этот раздел, вы можете использоватьMainScope
вместо него.
В зависимости от версии Android Studio, которую вы используете, вполне вероятно, что вам будет предложено заменить предыдущую реализацию на:
public suspend fun fetchLinkImage(link: String): String { return withContext(scope.coroutineContext) { feed.invokeFetchImageUrlFromLink( link ) } }
Оба подхода дают похожие результаты, но внутри они совершенно разные.
Вы можете удалить onNewImageUrlAvailable
из интерфейса FeedData.kt , расположенного в каталоге domain/cb .
Откройте GetFeedData.kt и обновите invokeFetchImageUrlFromLink
до следующего:
public suspend fun invokeFetchImageUrlFromLink( link: String ): String { return try { val result = FeedAPI.fetchImageUrlFromLink(link) parsePage(result.bodyAsText()) } catch (e: Exception) { "" } }
Теперь пришло время обновить интерфейс! Вам нужно будет изменить способ вызова fetchLinkImage
функции:
- Как в androidApp , так и в desktopApp перейдите к файлу FeedViewModel.kt внутри ui/home и замените существующую
fetchLinkImage
функцию на:
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 } }
Это код, который включает onNewImageUrlAvailable
, наряду с вызовом presenter.fetchLinkImage
. Поскольку вы больше не используете этот обратный вызов, вы можете удалить его.
- Для iOSApp также необходимо обновить файл FeedClient.swift , который находится внутри папки extensions . Начните с обновления,
FeedHandlerImage
которому больше не нужно получать все свои параметры:
public typealias FeedHandlerImage = (_ url: String) -> Void
Обновите fetchLinkImage
до:
@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") } } }
Поскольку теперь вы получаете доступ к функции приостановки из Swift, вам придется использовать ожидание, чтобы дождаться , пока результат будет доступен. @MainActor
Аннотация гарантирует выполнение в Task
потоке пользовательского интерфейса. В противном случае у вас может быть InvalidMutabilityException
.
Теперь удалите onNewImageUrlAvailable
из FeedClient
расширения в нижней части файла, так как этот обратный вызов больше не существует.
Поскольку эта функция должна быть объявлена как @MainActor
и id
, platform
и cb
больше не нужна, вам необходимо обновить fetchFeedsWithPreview
RWEntryViewModel.swift в корневой папке iosApp :
@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 } } } }
Скомпилируйте и запустите свое приложение, а также просмотрите великолепные иллюстрации статей на raywenderlich.com. :]

Рис. 13.9 — Android-приложение: просмотр последних статей

Рис. 13.10 — Приложение для ПК: просмотр последних статей

Рис. 13.11 — Приложение для iOS: просмотр последних статей
Новая модель памяти Kotlin/Native
В этой книге вы видели пару сценариев, в которых вам нужно было создать конкретную реализацию для iOS:
@ThreadLocal
: Использование этой аннотации в объекте гарантирует, что он не будет использоваться другими потоками, пытающимися получить к нему доступ. Вместо этого будет создана новая копия, гарантирующая, что объект не зависнет (в разделе «Подключение к API с помощью Ktor» из главы 12 «Сеть»).Dispatcher.Main
: в iOS нет поддержки запуска сопрограммы непосредственно в потоке пользовательского интерфейса. Для этого вам потребуется реализовать диспетчер на уровне платформы, как вы читали в разделе «Реализация Dispatchers.Main для iOS» в этой главе.
Эта новая модель памяти Kotlin/Native направлена на сокращение изменений, которые вам придется делать специально для iOS.
Хотя он все еще находится в экспериментальном состоянии, вы можете попробовать его в своих приложениях.
Включение новой модели памяти Kotlin/Native
Learn уже использует последние библиотеки, совместимые с новой моделью памяти Kotlin/Native:
kotlin-gradle-plugin
: 1.6.10ktor
: 2.0.0-бета-1coroutines-native-mt
: 1.6.0
Вам все еще нужно использовать native-mt
ветку, потому что korio
библиотека была построена с использованием более старой версии сопрограмм.
Откройте файл gradle.properties , расположенный в корневой папке, и добавьте:
#Enable Kotlin/Native Memory Model kotlin.native.binary.memoryModel=experimental kotlin.native.binary.freezing=disabled
Это активирует новую модель памяти и отключает замораживание. Вам нужно установить этот последний атрибут, потому что не все библиотеки полностью совместимы с новой моделью. Если вы не отключите freezing
, в конечном итоге вы окажетесь в своем приложении InvalidMutabilityException
для FreezingException
iOS.
Чтобы убедиться, что все работает должным образом, вы можете открыть файл PlatformDispatcher.kt в iosMain/domain в общем модуле и заменить геттер на:
get() = Dispatchers.Default
Dispatchers.IO
Для Native по-прежнему нет , но теперь вы можете использовать Default
вместо того, чтобы всегда использовать основной поток.
С этим изменением вам необходимо обновить вызывающую функцию fetchFeeds
из RWEntryViewModel в приложении iOS:
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 } } }
Запустите приложение для iOS. Перейдите через приложение, чтобы убедиться, что все работает должным образом.

Рис. 13.12 — Приложение для iOS: просмотр последних статей
Испытание
Перед вами стоит задача применить на практике то, что вы узнали в этой главе. Если вы застряли на каком-либо этапе, взгляните на решения в материалах к этой главе.
Задача: получить изображения статьи из общего модуля
Вместо того, чтобы запрашивать изображения статей из пользовательского интерфейса, переместите эту логику в общий модуль.
Помните, что вам не нужно запускать эту логику последовательно — вы можете запустить несколько сопрограмм для получения и анализа ответа, что ускорит эту операцию.
Запросы должны выполняться параллельно.
Ключевые моменты
- Функция приостановки может быть вызвана только из другой функции приостановки или из сопрограммы.
- Вы можете использовать
launch
илиasync
для создания и запуска сопрограммы. - Сопрограмма может запускать поток из пулов потоков Main , IO или Default .
- Новая модель памяти Kotlin/Native позволяет запускать несколько потоков на iOS.
Куда пойти отсюда?
Вы узнали, как реализовать асинхронные запросы с помощью сопрограмм и как работать с параллелизмом. Если вы хотите глубже погрузиться в эту тему, попробуйте книгу Kotlin Coroutines, где вы можете более подробно прочитать о Coroutines, Channels и Flows в Android. Есть также Concurrency, который фокусируется на многопоточности в Swift, и Concurrency в Swift , который обучает вас новой модели параллелизма с синтаксисом async/away .
В следующей главе вы узнаете, как перенести функцию для поддержки Kotlin Multiplatform и выпустить свои библиотеки, чтобы позже вы могли повторно использовать их в своих проектах.