HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>В сети хватает базовых примеров приложений на KMM, поэтому мы рассмотрим что-то, более приближенное к нашим ежедневным задачам разработчика, а именно, как реализовать<strong>многопоточное приложение на Kotlin Multiplatform</strong>.</p>
1 <p>В сети хватает базовых примеров приложений на KMM, поэтому мы рассмотрим что-то, более приближенное к нашим ежедневным задачам разработчика, а именно, как реализовать<strong>многопоточное приложение на Kotlin Multiplatform</strong>.</p>
2 <p>Основная идея KMM, как и других кросс-платформенных технологий - оптимизация разработки путем написания кода один раз и последующего его использования на разных платформах.</p>
2 <p>Основная идея KMM, как и других кросс-платформенных технологий - оптимизация разработки путем написания кода один раз и последующего его использования на разных платформах.</p>
3 <p>Согласно концепции JetBrains, Kotlin Multiplatform не является фреймворком. Это именно SDK, который позволяет создавать модули с общим кодом, подключаемые к нативным приложениям.</p>
3 <p>Согласно концепции JetBrains, Kotlin Multiplatform не является фреймворком. Это именно SDK, который позволяет создавать модули с общим кодом, подключаемые к нативным приложениям.</p>
4 <p>Для взаимодействия с платформами используются специфические для платформы версии Kotlin: Kotlin/JVM, Kotlin/JS, Kotlin/Native. Данные версии включают расширения языка Kotlin, а также специфичные для конкретной платформы библиотеки и инструменты. Написанный на Kotlin модуль компилируется в JVM байткод для Android и LLVM байткод для iOS.</p>
4 <p>Для взаимодействия с платформами используются специфические для платформы версии Kotlin: Kotlin/JVM, Kotlin/JS, Kotlin/Native. Данные версии включают расширения языка Kotlin, а также специфичные для конкретной платформы библиотеки и инструменты. Написанный на Kotlin модуль компилируется в JVM байткод для Android и LLVM байткод для iOS.</p>
5 <p>Модуль (Shared, Common) содержит переиспользуемую бизнес-логику. Платформенные модули iOS/Android, к которым подключен Shared/Common, либо используют написанную логику напрямую, либо имплементируют свою реализацию в зависимости от особенностей платформы.</p>
5 <p>Модуль (Shared, Common) содержит переиспользуемую бизнес-логику. Платформенные модули iOS/Android, к которым подключен Shared/Common, либо используют написанную логику напрямую, либо имплементируют свою реализацию в зависимости от особенностей платформы.</p>
6 <p>Общая бизнес-логика может включать в себя:</p>
6 <p>Общая бизнес-логика может включать в себя:</p>
7 <ul><li>сервисы для работы с сетью;</li>
7 <ul><li>сервисы для работы с сетью;</li>
8 <li>сервисы для работы с БД;</li>
8 <li>сервисы для работы с БД;</li>
9 <li>модели данных.</li>
9 <li>модели данных.</li>
10 </ul><p>Также в нее могут входить архитектурные компоненты приложения, напрямую не включающие UI, но с ним взаимодействующие:</p>
10 </ul><p>Также в нее могут входить архитектурные компоненты приложения, напрямую не включающие UI, но с ним взаимодействующие:</p>
11 <ul><li>ViewModel;</li>
11 <ul><li>ViewModel;</li>
12 <li>Presenter;</li>
12 <li>Presenter;</li>
13 <li>Интеракторы и т.п.</li>
13 <li>Интеракторы и т.п.</li>
14 </ul><p>Концепцию Kotlin Multiplatform можно сравнить с реализацией Xamarin Native. Однако, здесь нет модулей или функционала, реализующих UI. Эта логическая нагрузка ложится на подключенные нативные проекты.</p>
14 </ul><p>Концепцию Kotlin Multiplatform можно сравнить с реализацией Xamarin Native. Однако, здесь нет модулей или функционала, реализующих UI. Эта логическая нагрузка ложится на подключенные нативные проекты.</p>
15 <p>Теперь рассмотрим подход на практике.</p>
15 <p>Теперь рассмотрим подход на практике.</p>
16 <p>Если вы еще не работали с KMM, то потребуется установить и настроить инструменты. Раньше это было довольно хлопотно, но сейчас достаточно установить Android Studio (версии от 4.1) и плагин Kotlin Multiplatform Mobile . Выбираем шаблон KMM Application при создании проекта, и все отработает автоматически.</p>
16 <p>Если вы еще не работали с KMM, то потребуется установить и настроить инструменты. Раньше это было довольно хлопотно, но сейчас достаточно установить Android Studio (версии от 4.1) и плагин Kotlin Multiplatform Mobile . Выбираем шаблон KMM Application при создании проекта, и все отработает автоматически.</p>
17 <p>Мультиплатформенные проекты Kotlin обычно делятся на несколько модулей:</p>
17 <p>Мультиплатформенные проекты Kotlin обычно делятся на несколько модулей:</p>
18 <ul><li>модуль переиспользуемой бизнес-логики (Shared, commonMain и т.п);</li>
18 <ul><li>модуль переиспользуемой бизнес-логики (Shared, commonMain и т.п);</li>
19 <li>модуль для IOS приложения (iOSMain, iOSTest);</li>
19 <li>модуль для IOS приложения (iOSMain, iOSTest);</li>
20 <li>модуль для Android приложения (androidMain, androidTest).</li>
20 <li>модуль для Android приложения (androidMain, androidTest).</li>
21 </ul><p>В них располагается наша бизнес-логика. Всю используемую в проекте бизнес-логику можно разделить на:</p>
21 </ul><p>В них располагается наша бизнес-логика. Всю используемую в проекте бизнес-логику можно разделить на:</p>
22 <ul><li>переиспользуемую (общую);</li>
22 <ul><li>переиспользуемую (общую);</li>
23 <li>платформенную реализацию.</li>
23 <li>платформенную реализацию.</li>
24 </ul><p>Переиспользуемая логика располагается в проекте commonMain в каталоге kotlin и разделяется на package. Декларации функций, классов и объектов, обязательных к переопределению, помечаются модификатором expect:</p>
24 </ul><p>Переиспользуемая логика располагается в проекте commonMain в каталоге kotlin и разделяется на package. Декларации функций, классов и объектов, обязательных к переопределению, помечаются модификатором expect:</p>
25 <p>Реализации должны иметь модификатор actual.</p>
25 <p>Реализации должны иметь модификатор actual.</p>
26 <p>В качестве примера работы с многопоточностью рассмотрим небольшое приложение, обращающееся к стороннему API по сети:</p>
26 <p>В качестве примера работы с многопоточностью рассмотрим небольшое приложение, обращающееся к стороннему API по сети:</p>
27 <p>Я выбрала www.themoviedb.org. Полный код примера будет по ссылке внизу статьи.</p>
27 <p>Я выбрала www.themoviedb.org. Полный код примера будет по ссылке внизу статьи.</p>
28 <p>В общей Common части расположим общую бизнес-логику:</p>
28 <p>В общей Common части расположим общую бизнес-логику:</p>
29 <p>А именно наш сетевой сервис. Это логично.</p>
29 <p>А именно наш сетевой сервис. Это логично.</p>
30 <p>В модулях iOS/Android приложений оставим только UI компоненты для отображения списка и адаптеры. iOS часть будет написана на Swift, Android - на Kotlin.</p>
30 <p>В модулях iOS/Android приложений оставим только UI компоненты для отображения списка и адаптеры. iOS часть будет написана на Swift, Android - на Kotlin.</p>
31 <p>Начнем с бизнес-логики. Т.к весь функционал будет в модуле common, то мы будем использовать в качестве библиотек решения для Kotlin Multiplatform:</p>
31 <p>Начнем с бизнес-логики. Т.к весь функционал будет в модуле common, то мы будем использовать в качестве библиотек решения для Kotlin Multiplatform:</p>
32 <p>Ktor - библиотека для работы с сетью и сериализации.</p>
32 <p>Ktor - библиотека для работы с сетью и сериализации.</p>
33 <p>В build.gradle (:app) пропишем следующие зависимости:</p>
33 <p>В build.gradle (:app) пропишем следующие зависимости:</p>
34 val ktorVersion = "1.4.0" val serializationVersion = "1.0.0-RC" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") implementation("io.ktor:ktor-client-serialization:$ktorVersion") } } val androidMain by getting { dependencies { //... implementation("io.ktor:ktor-client-android:$ktorVersion") } } val iosMain by getting { dependencies { implementation("io.ktor:ktor-client-ios:$ktorVersion") } } ...<p>Также добавим поддержку сериализации:</p>
34 val ktorVersion = "1.4.0" val serializationVersion = "1.0.0-RC" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") implementation("io.ktor:ktor-client-serialization:$ktorVersion") } } val androidMain by getting { dependencies { //... implementation("io.ktor:ktor-client-android:$ktorVersion") } } val iosMain by getting { dependencies { implementation("io.ktor:ktor-client-ios:$ktorVersion") } } ...<p>Также добавим поддержку сериализации:</p>
35 plugins { //... kotlin("plugin.serialization") version "1.4.10" }<p>Далее нам надо определить, что делать с многопоточностью, ведь она реализуется по-разному на каждой платформе. На стороне iOS мы используем GCD (Grand Central Dispatch), а на стороне Android - JVM Threads и Coroutines. Однако, в Kotlin Multiplatform мы можем сделать общей и работу с многопоточностью.</p>
35 plugins { //... kotlin("plugin.serialization") version "1.4.10" }<p>Далее нам надо определить, что делать с многопоточностью, ведь она реализуется по-разному на каждой платформе. На стороне iOS мы используем GCD (Grand Central Dispatch), а на стороне Android - JVM Threads и Coroutines. Однако, в Kotlin Multiplatform мы можем сделать общей и работу с многопоточностью.</p>
36 <p>Для этого мы будет использовать Kotlin Coroutines:</p>
36 <p>Для этого мы будет использовать Kotlin Coroutines:</p>
37 val coroutinesVersion = "1.3.9-native-mt" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") //... } } val androidMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") //... } } val iosMain by getting { dependencies { //... } } ...<p>Тут стоит сделать пояснение, как с этим работать, потому что далеко не все iOS разработчики знают, что такое Coroutines. Если вкратце, то это блок кода, который можно приостановить, не блокируя поток. У корутины может быть контекст выполнения (CoroutineContext), цикл жизни корутины управляется Job. У корутины есть область действия (CoroutineScope), а поток, в котором она исполняется, задается с помощью CoroutineDispatcher.</p>
37 val coroutinesVersion = "1.3.9-native-mt" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") //... } } val androidMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") //... } } val iosMain by getting { dependencies { //... } } ...<p>Тут стоит сделать пояснение, как с этим работать, потому что далеко не все iOS разработчики знают, что такое Coroutines. Если вкратце, то это блок кода, который можно приостановить, не блокируя поток. У корутины может быть контекст выполнения (CoroutineContext), цикл жизни корутины управляется Job. У корутины есть область действия (CoroutineScope), а поток, в котором она исполняется, задается с помощью CoroutineDispatcher.</p>
38 <p>Если проводить аналогию с iOS, то это похоже на выполнение блока кода в DispatchQueue, имеющей определенный QoS и привязку к определенному потоку NSThread, либо Operation в OperationQueue, где GlobalScope аналогичен DispatchQueue.global(), а MainScope - DispatchQueue.main:</p>
38 <p>Если проводить аналогию с iOS, то это похоже на выполнение блока кода в DispatchQueue, имеющей определенный QoS и привязку к определенному потоку NSThread, либо Operation в OperationQueue, где GlobalScope аналогичен DispatchQueue.global(), а MainScope - DispatchQueue.main:</p>
39 //Android fun loadMovies() { GlobalScope.async { service.makeRequest() withContext(uiDispatcher) { //... } } } //iOS func loadMovies() { DispatchQueue.global().async { service.makeRequest() DispatchQueue.main.async{ //... } } }<p>Еще одной ключевой особенностью корутин является использование слова suspend. Данный модификатор не превращает метод в асинхронный сам по себе, это зависит от других деталей реализации, но маркирует, что его можно приостановить без блокировки потока. Также такой метод можно вызывать только в контексте корутины.</p>
39 //Android fun loadMovies() { GlobalScope.async { service.makeRequest() withContext(uiDispatcher) { //... } } } //iOS func loadMovies() { DispatchQueue.global().async { service.makeRequest() DispatchQueue.main.async{ //... } } }<p>Еще одной ключевой особенностью корутин является использование слова suspend. Данный модификатор не превращает метод в асинхронный сам по себе, это зависит от других деталей реализации, но маркирует, что его можно приостановить без блокировки потока. Также такой метод можно вызывать только в контексте корутины.</p>
40 <p>Ktor использует механизм корутины для реализации асинхронной работы, поэтому вызов HttpClient делаем в suspended функции:</p>
40 <p>Ktor использует механизм корутины для реализации асинхронной работы, поэтому вызов HttpClient делаем в suspended функции:</p>
41 //Network service class NetworkService { val httpClient = HttpClient { install(JsonFeature) { val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } serializer = KotlinxSerializer(json) } } suspend inline fun &lt;reified T&gt; loadData(url: String): T? { return httpClient.get(url) } } //Movies service suspend fun loadMovies():MoviesList? { val url = MY_URL return networkService.loadData&lt;MoviesList&gt;(url) }<p>При подключении Kotlin Coroutines мы не указали никакую особую версию для iOS. Это не ошибка. Дело в том, что начиная с версии Kotlin 1.4 Suspended функция Kotlin легко трансформируется в функцию Swift c completion handler блоком:</p>
41 //Network service class NetworkService { val httpClient = HttpClient { install(JsonFeature) { val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } serializer = KotlinxSerializer(json) } } suspend inline fun &lt;reified T&gt; loadData(url: String): T? { return httpClient.get(url) } } //Movies service suspend fun loadMovies():MoviesList? { val url = MY_URL return networkService.loadData&lt;MoviesList&gt;(url) }<p>При подключении Kotlin Coroutines мы не указали никакую особую версию для iOS. Это не ошибка. Дело в том, что начиная с версии Kotlin 1.4 Suspended функция Kotlin легко трансформируется в функцию Swift c completion handler блоком:</p>
42 func getMovies() { self.networkService?.loadMovies {(movies, error) in //... } }<p>Т. к. Ktor уже обеспечивает асинхронность, то в данном случае потребности в использовании DispatchQueue на стороне iOS нет.</p>
42 func getMovies() { self.networkService?.loadMovies {(movies, error) in //... } }<p>Т. к. Ktor уже обеспечивает асинхронность, то в данном случае потребности в использовании DispatchQueue на стороне iOS нет.</p>
43 <p>На стороне Android используем механизм корутинов, и вызов будет иметь вид:</p>
43 <p>На стороне Android используем механизм корутинов, и вызов будет иметь вид:</p>
44 fun getMovies() { mainScope.launch { val movies = this.networkService?.loadMovies() //... } } }<p>Такой способ обращения к общей логике мы можем использовать при подходе, когда у нас нет общего архитектурного элемента наших нативных приложений, и в Common проекте мы реализуем только бизнес-логику. Это вполне рабочий подход.</p>
44 fun getMovies() { mainScope.launch { val movies = this.networkService?.loadMovies() //... } } }<p>Такой способ обращения к общей логике мы можем использовать при подходе, когда у нас нет общего архитектурного элемента наших нативных приложений, и в Common проекте мы реализуем только бизнес-логику. Это вполне рабочий подход.</p>
45 <p>Если же мы хотим сделать максимально расшариваемый между наивными проектами общий код, включить туда архитектурное решение, а взаимодействие с UI через протоколы, то нам потребуется поменять работу и с потоками.</p>
45 <p>Если же мы хотим сделать максимально расшариваемый между наивными проектами общий код, включить туда архитектурное решение, а взаимодействие с UI через протоколы, то нам потребуется поменять работу и с потоками.</p>
46 <p>Посмотрим это в следующей части, следите за обновлениями блога!</p>
46 <p>Посмотрим это в следующей части, следите за обновлениями блога!</p>
47 <p>Исходники примера находятся здесь: github.com/anioutkazharkova/movies_kmp.</p>
47 <p>Исходники примера находятся здесь: github.com/anioutkazharkova/movies_kmp.</p>
48  
48