HTML Diff
1 added 1 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>В настоящее время мы переживаем бум появления новых технологий и подходов к написанию мобильных приложений. Одной из них является развивающийся SDK от компании JetBrains для мультиплатформенной разработки Kotlin Multiplatfrom (KMP).</p>
1 <p>В настоящее время мы переживаем бум появления новых технологий и подходов к написанию мобильных приложений. Одной из них является развивающийся SDK от компании JetBrains для мультиплатформенной разработки Kotlin Multiplatfrom (KMP).</p>
2 <p>Основная идея KMP, как и других кросс-платформенных SDK - оптимизация разработки путем написания кода один раз и последующего его использования на разных платформах.</p>
2 <p>Основная идея KMP, как и других кросс-платформенных SDK - оптимизация разработки путем написания кода один раз и последующего его использования на разных платформах.</p>
3 <p>Согласно концепции JetBrains, Kotlin Multiplatform не является фреймворком. Это именно SDK, который позволяет создавать модули с общим кодом, подключаемые к нативным приложениям.</p>
3 <p>Согласно концепции JetBrains, Kotlin Multiplatform не является фреймворком. Это именно SDK, который позволяет создавать модули с общим кодом, подключаемые к нативным приложениям.</p>
4 <p>Написанный на Kotlin модуль компилируется в JVM байткод для Android и LLVM байткод для iOS.</p>
4 <p>Написанный на 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. Однако, в KMP нет модулей или функционала, реализующих UI. Эта логическая нагрузка ложится на подключенные нативные проекты.</p>
14 </ul><p>Концепцию Kotlin Multiplatform можно сравнить с реализацией Xamarin Native. Однако, в KMP нет модулей или функционала, реализующих UI. Эта логическая нагрузка ложится на подключенные нативные проекты.</p>
15 <p><strong>Рассмотрим подход на практике и попробуем написать наше первое приложение Kotlin Multiplatform.</strong></p>
15 <p><strong>Рассмотрим подход на практике и попробуем написать наше первое приложение Kotlin Multiplatform.</strong></p>
16 <p>Для начала нам потребуется установить и настроить инструменты:</p>
16 <p>Для начала нам потребуется установить и настроить инструменты:</p>
17 <ol><li>Android Sdk</li>
17 <ol><li>Android Sdk</li>
18 <li>Xcode с последним iOS SDK.</li>
18 <li>Xcode с последним iOS SDK.</li>
19 <li>Intelij IDEA CE или Android Studio. Обе IDE позволяют создавать и настраивать проекты для Kotlin Multiplatform. Но если в Intelij IDEA проект создается автоматически, то в Android Studio большую часть настроек надо сделать вручную. Если вам привычнее работать именно с Android Studio, то подробное руководство по созданию проекта можно посмотреть в документации на Kotlinlang.org</li>
19 <li>Intelij IDEA CE или Android Studio. Обе IDE позволяют создавать и настраивать проекты для Kotlin Multiplatform. Но если в Intelij IDEA проект создается автоматически, то в Android Studio большую часть настроек надо сделать вручную. Если вам привычнее работать именно с Android Studio, то подробное руководство по созданию проекта можно посмотреть в документации на Kotlinlang.org</li>
20 </ol><p>Мы рассмотрим создание проекта с помощью Intelij IDEA.</p>
20 </ol><p>Мы рассмотрим создание проекта с помощью Intelij IDEA.</p>
21 <p>Выбираем меню File → New → Create Project:</p>
21 <p>Выбираем меню File → New → Create Project:</p>
22 <p>В появившемся окне выбираем тип проекта Kotlin → Mobile Android/iOS|Gradle</p>
22 <p>В появившемся окне выбираем тип проекта Kotlin → Mobile Android/iOS|Gradle</p>
23 <p>Далее стандартно задаем путь к JDK, имя и расположение проекта:</p>
23 <p>Далее стандартно задаем путь к JDK, имя и расположение проекта:</p>
24 <p>После нажатия кнопки Finish проект сгенерируется и будет почти готов к работе.</p>
24 <p>После нажатия кнопки Finish проект сгенерируется и будет почти готов к работе.</p>
25 <p>Рассмотрим, что у нас получилось:</p>
25 <p>Рассмотрим, что у нас получилось:</p>
26 <p>Мультиплатформенные проекты Kotlin обычно делятся на несколько модулей:</p>
26 <p>Мультиплатформенные проекты Kotlin обычно делятся на несколько модулей:</p>
27 <ul><li>модуль переиспользуемой бизнес-логики (Shared, commonMain и т.п);</li>
27 <ul><li>модуль переиспользуемой бизнес-логики (Shared, commonMain и т.п);</li>
28 <li>модуль для IOS приложения (iOSMain, iOSTest);</li>
28 <li>модуль для IOS приложения (iOSMain, iOSTest);</li>
29 <li>модуль для Android приложения (androidMain, androidTest).</li>
29 <li>модуль для Android приложения (androidMain, androidTest).</li>
30 </ul><p>В них располагается наша бизнес-логика. Сам код базового примера мы разберем немного позже.</p>
30 </ul><p>В них располагается наша бизнес-логика. Сам код базового примера мы разберем немного позже.</p>
31 <p>Код нативного Android приложения располагается в каталоге main, как если бы мы создавали проект по шаблону обычного Android.</p>
31 <p>Код нативного Android приложения располагается в каталоге main, как если бы мы создавали проект по шаблону обычного Android.</p>
32 <p>iOS приложение создается автоматически и располагается в каталоге iOSApp:</p>
32 <p>iOS приложение создается автоматически и располагается в каталоге iOSApp:</p>
33 <p>Перед тем, как мы проверим работоспособность базового решения, необходимо сделать ряд финальных настроек.</p>
33 <p>Перед тем, как мы проверим работоспособность базового решения, необходимо сделать ряд финальных настроек.</p>
34 <p>В local.properties зададим путь к SDK Android:</p>
34 <p>В local.properties зададим путь к SDK Android:</p>
35 <p>Создадим конфигурацию для работы Android-приложения:</p>
35 <p>Создадим конфигурацию для работы Android-приложения:</p>
36 <p>Готово.</p>
36 <p>Готово.</p>
37 <p>Теперь вызовем команду gradle wrapper для сборки нашего модуля общей логики:</p>
37 <p>Теперь вызовем команду gradle wrapper для сборки нашего модуля общей логики:</p>
38 <p>После сборки модуль для бизнес-логики для Android приложения доступен в app/build/libs:</p>
38 <p>После сборки модуль для бизнес-логики для Android приложения доступен в app/build/libs:</p>
39 <p>Путь к библиотеке прописывается стандартно, в блоке dependencies файла build.gradle:</p>
39 <p>Путь к библиотеке прописывается стандартно, в блоке dependencies файла build.gradle:</p>
40 <p>Теперь наш проект сконфигурирован для запуска Android-приложения:</p>
40 <p>Теперь наш проект сконфигурирован для запуска Android-приложения:</p>
41 - <p>Осталось сделать настройки для запуска приложения iOS.</p>
41 + <p>Осталось сделать настройки для запука приложения iOS.</p>
42 <p>В файле build.gradle(:app) необходимо изменить настройку архитектура проекта, чтобы наше приложение поддерживало как реальные устройства, так и эмуляторы.</p>
42 <p>В файле build.gradle(:app) необходимо изменить настройку архитектура проекта, чтобы наше приложение поддерживало как реальные устройства, так и эмуляторы.</p>
43 <p>Меняем:</p>
43 <p>Меняем:</p>
44 <p>На:</p>
44 <p>На:</p>
45 <p>После выполнения сборки создастся фреймворк в app/build/bin/ios:</p>
45 <p>После выполнения сборки создастся фреймворк в app/build/bin/ios:</p>
46 <p>Intelij IDEA автоматически создает в gradle-файле код для генерации, подключения и встраивания фреймворка в iOS-проект:</p>
46 <p>Intelij IDEA автоматически создает в gradle-файле код для генерации, подключения и встраивания фреймворка в iOS-проект:</p>
47 <p>При ручной настройке проекта (например, через Android Studio) этот код потребуется указать самостоятельно.</p>
47 <p>При ручной настройке проекта (например, через Android Studio) этот код потребуется указать самостоятельно.</p>
48 <p>После синхронизации gradle iOS-проект готов к запуску и проверке с помощью XCode.</p>
48 <p>После синхронизации gradle iOS-проект готов к запуску и проверке с помощью XCode.</p>
49 <p>Проверяем, что у нас получилось. Открываем проект iOS через iosApp.xcodeproj:</p>
49 <p>Проверяем, что у нас получилось. Открываем проект iOS через iosApp.xcodeproj:</p>
50 <p>Проект имеет стандартную структуру, за исключением раздела app, где мы получаем доступ к коду наших модулей на Kotlin.</p>
50 <p>Проект имеет стандартную структуру, за исключением раздела app, где мы получаем доступ к коду наших модулей на Kotlin.</p>
51 <p>Фреймворк действительно подключен автоматически во всех соответствующих разделах проекта:</p>
51 <p>Фреймворк действительно подключен автоматически во всех соответствующих разделах проекта:</p>
52 <p>Запускаем проект на эмуляторе:</p>
52 <p>Запускаем проект на эмуляторе:</p>
53 <p>Теперь разберем код самого приложения на базовом примере.</p>
53 <p>Теперь разберем код самого приложения на базовом примере.</p>
54 <p>Используемую в проекте бизнес-логику можно разделить на:</p>
54 <p>Используемую в проекте бизнес-логику можно разделить на:</p>
55 <ul><li>переиспользуемую (общую);</li>
55 <ul><li>переиспользуемую (общую);</li>
56 <li>платформенную реализацию.</li>
56 <li>платформенную реализацию.</li>
57 </ul><p>Переиспользуемая логика располагается в проекте commonMain в каталоге kotlin и разделяется на package. Декларации функций, классов и объектов, обязательных к переопределению, помечаются модификатором expect:</p>
57 </ul><p>Переиспользуемая логика располагается в проекте commonMain в каталоге kotlin и разделяется на package. Декларации функций, классов и объектов, обязательных к переопределению, помечаются модификатором expect:</p>
58 <p>Реализация expect-функционала задается в платформенных модулях и помечается модификатором actual:</p>
58 <p>Реализация expect-функционала задается в платформенных модулях и помечается модификатором actual:</p>
59 <p>Вызов логики производится в нативном проекте:</p>
59 <p>Вызов логики производится в нативном проекте:</p>
60 <p>Все очень просто.</p>
60 <p>Все очень просто.</p>
61 <p><strong>Теперь попробуем по тем же принципам сделать что-то посложнее и поинтереснее. Например, небольшое приложение для получения и отображение списка новостей для iOS и Android.</strong></p>
61 <p><strong>Теперь попробуем по тем же принципам сделать что-то посложнее и поинтереснее. Например, небольшое приложение для получения и отображение списка новостей для iOS и Android.</strong></p>
62 <p>Приложение будет иметь следующую структуру:</p>
62 <p>Приложение будет иметь следующую структуру:</p>
63 <p>В общей части (Common) расположим бизнес-логику:</p>
63 <p>В общей части (Common) расположим бизнес-логику:</p>
64 <p><strong>сетевой сервис; сервис для запросов новостей.</strong></p>
64 <p><strong>сетевой сервис; сервис для запросов новостей.</strong></p>
65 <p>В модулях iOS/Android-приложений оставим только UI-компоненты для отображения списка и адаптеры. iOS-часть будет написана на Swift, Android - на Kotlin. Здесь в плане работы не будет ничего нового.</p>
65 <p>В модулях iOS/Android-приложений оставим только UI-компоненты для отображения списка и адаптеры. iOS-часть будет написана на Swift, Android - на Kotlin. Здесь в плане работы не будет ничего нового.</p>
66 <p>Организуем архитектуру приложений по простому паттерну MVP. Презентер, обращающийся к бизнес-логике, также вынесем в Common часть. Также поступим и с протоколом для связи между презентером и экранами UI:</p>
66 <p>Организуем архитектуру приложений по простому паттерну MVP. Презентер, обращающийся к бизнес-логике, также вынесем в Common часть. Также поступим и с протоколом для связи между презентером и экранами UI:</p>
67 interface INewsListView :IView { fun setupNews(list: List) }<p><strong>Начнем с бизнес-логики</strong>. Т. к. весь функционал будет в модуле common, то мы будем использовать в качестве библиотек решения для Kotlin Multiplatform:</p>
67 interface INewsListView :IView { fun setupNews(list: List) }<p><strong>Начнем с бизнес-логики</strong>. Т. к. весь функционал будет в модуле common, то мы будем использовать в качестве библиотек решения для Kotlin Multiplatform:</p>
68 <p>1.Ktor - библиотека для работы с сетью и сериализации.</p>
68 <p>1.Ktor - библиотека для работы с сетью и сериализации.</p>
69 <p>В build.gradle (:app) пропишем следующие зависимости:</p>
69 <p>В build.gradle (:app) пропишем следующие зависимости:</p>
70 commonMain { dependencies { … implementation("io.ktor:ktor-client-core:1.3.2") implementation("io.ktor:ktor-client-json:1.3.2") implementation("io.ktor:ktor-client-serialization:1.3.2") } } androidMain { dependencies { ….. implementation("io.ktor:ktor-client-android:1.3.2") implementation("io.ktor:ktor-client-json-jvm:1.3.2") implementation("io.ktor:ktor-client-serialization-jvm:1.3.2") } } iosMain { dependencies { //…. implementation("io.ktor:ktor-client-ios:1.3.2") implementation("io.ktor:ktor-client-json-native:1.3.2") implementation("io.ktor:ktor-client-serialization-native:1.3.2") } }<p>Также добавим поддержку плагина сериализации:</p>
70 commonMain { dependencies { … implementation("io.ktor:ktor-client-core:1.3.2") implementation("io.ktor:ktor-client-json:1.3.2") implementation("io.ktor:ktor-client-serialization:1.3.2") } } androidMain { dependencies { ….. implementation("io.ktor:ktor-client-android:1.3.2") implementation("io.ktor:ktor-client-json-jvm:1.3.2") implementation("io.ktor:ktor-client-serialization-jvm:1.3.2") } } iosMain { dependencies { //…. implementation("io.ktor:ktor-client-ios:1.3.2") implementation("io.ktor:ktor-client-json-native:1.3.2") implementation("io.ktor:ktor-client-serialization-native:1.3.2") } }<p>Также добавим поддержку плагина сериализации:</p>
71 plugins { …. id 'org.jetbrains.kotlin.plugin.serialization' version '1.3.72' } apply plugin: 'kotlinx-serialization'<p>2.Kotlin Coroutines - для организации многопоточной работы.</p>
71 plugins { …. id 'org.jetbrains.kotlin.plugin.serialization' version '1.3.72' } apply plugin: 'kotlinx-serialization'<p>2.Kotlin Coroutines - для организации многопоточной работы.</p>
72 commonMain { dependencies { … implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.7") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:0.14.0") …. } } androidMain { dependencies { … implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0") …. } } iosMain { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.5-native-mt") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:0.14.0") …. } }<p>При добавлении зависимости в iOS-проект обратите внимание, что версия библиотеки должна быть обязательно native-mt и совместима с версией плагина Kotlin multiplatform.</p>
72 commonMain { dependencies { … implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.7") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:0.14.0") …. } } androidMain { dependencies { … implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0") …. } } iosMain { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.5-native-mt") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:0.14.0") …. } }<p>При добавлении зависимости в iOS-проект обратите внимание, что версия библиотеки должна быть обязательно native-mt и совместима с версией плагина Kotlin multiplatform.</p>
73 <p>При организации многопоточности с помощью Coroutines необходимо передавать контекст потока (CoroutineContext), в котором логика будет исполняться. Это платформозависимая логика, поэтому используем кастомизацию с помощью expect/actual.</p>
73 <p>При организации многопоточности с помощью Coroutines необходимо передавать контекст потока (CoroutineContext), в котором логика будет исполняться. Это платформозависимая логика, поэтому используем кастомизацию с помощью expect/actual.</p>
74 <p>В commonMain создадим Dispatchers.kt, где объявим переменные:</p>
74 <p>В commonMain создадим Dispatchers.kt, где объявим переменные:</p>
75 expect val defaultDispatcher: CoroutineContext expect val uiDispatcher: CoroutineContext<p>Реализация в androidMain создается легко. Для доступа к соответствующим потокам используем CoroutineDispatchers Main (UI поток) и Default (стандартный для Coroutine):</p>
75 expect val defaultDispatcher: CoroutineContext expect val uiDispatcher: CoroutineContext<p>Реализация в androidMain создается легко. Для доступа к соответствующим потокам используем CoroutineDispatchers Main (UI поток) и Default (стандартный для Coroutine):</p>
76 actual val uiDispatcher: CoroutineContext get() = Dispatchers.Main actual val defaultDispatcher: CoroutineContext get() = Dispatchers.Default<p>С iOS труднее. Та версия Kotlin Native LLVM компилятора, которая используется в Kotlin Multiplatform,<strong>не поддерживает background очереди</strong>. Это давно известная проблема, которая к сожалению, еще не исправлена</p>
76 actual val uiDispatcher: CoroutineContext get() = Dispatchers.Main actual val defaultDispatcher: CoroutineContext get() = Dispatchers.Default<p>С iOS труднее. Та версия Kotlin Native LLVM компилятора, которая используется в Kotlin Multiplatform,<strong>не поддерживает background очереди</strong>. Это давно известная проблема, которая к сожалению, еще не исправлена</p>
77 <p>Поэтому попробуем обходной маневр как временное решение проблемы.</p>
77 <p>Поэтому попробуем обходной маневр как временное решение проблемы.</p>
78 actual val uiDispatcher: CoroutineContext get() = MainDispatcher actual val defaultDispatcher: CoroutineContext get() = MainDispatcher 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 } } } }<p>Мы создаем свой CoroutineDispatcher, где прописываем выполнение логики в асинхронной очереди dispatch_async.</p>
78 actual val uiDispatcher: CoroutineContext get() = MainDispatcher actual val defaultDispatcher: CoroutineContext get() = MainDispatcher 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 } } } }<p>Мы создаем свой CoroutineDispatcher, где прописываем выполнение логики в асинхронной очереди dispatch_async.</p>
79 <p>Также нам понадобится свой scope для работы сетевого клиента:</p>
79 <p>Также нам понадобится свой scope для работы сетевого клиента:</p>
80 <h4>iOS</h4>
80 <h4>iOS</h4>
81 actual fun ktorScope(block: suspend () -&gt; Unit) { GlobalScope.launch(MainDispatcher) { block() } }<h4>Android</h4>
81 actual fun ktorScope(block: suspend () -&gt; Unit) { GlobalScope.launch(MainDispatcher) { block() } }<h4>Android</h4>
82 actual fun ktorScope(block: suspend () -&gt; Unit) { GlobalScope.launch(Dispatchers.Main) { block() } }<p>Применим это при реализации сетевого клиента на Ktor:</p>
82 actual fun ktorScope(block: suspend () -&gt; Unit) { GlobalScope.launch(Dispatchers.Main) { block() } }<p>Применим это при реализации сетевого клиента на Ktor:</p>
83 interface INetworkService { suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)-&gt;Unit) } class NetworkService : INetworkService{ private val httpClient = HttpClient() override suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)-&gt;Unit){ //Для ktor используем свой скоуп ktorScope { var contentResponse = ContentResponse() try { val json = httpClient.get { url { protocol = URLProtocol.HTTPS host = NetworkConfig.shared.apiUrl encodedPath = path header("X-Api-Key", NetworkConfig.shared.apiKey) } } print(json) val response = kotlinx.serialization.json.Json.nonstrict.parse(serializer, json) contentResponse.content = response } catch (ex: Exception) { val error = ErrorResponse() error.message = ex.message.toString() contentResponse.errorResponse = error print(ex.message.toString()) } //Ответ отдаем в UI-поток withContext(uiDispatcher) { completed(contentResponse) } } } }<p>Парсинг реализуем с помощью сериализатора типа KSerializer&lt;T&gt;. В нашем случае это NewsList.serializer(). Пропишем реализацию в сервисе новостей:</p>
83 interface INetworkService { suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)-&gt;Unit) } class NetworkService : INetworkService{ private val httpClient = HttpClient() override suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)-&gt;Unit){ //Для ktor используем свой скоуп ktorScope { var contentResponse = ContentResponse() try { val json = httpClient.get { url { protocol = URLProtocol.HTTPS host = NetworkConfig.shared.apiUrl encodedPath = path header("X-Api-Key", NetworkConfig.shared.apiKey) } } print(json) val response = kotlinx.serialization.json.Json.nonstrict.parse(serializer, json) contentResponse.content = response } catch (ex: Exception) { val error = ErrorResponse() error.message = ex.message.toString() contentResponse.errorResponse = error print(ex.message.toString()) } //Ответ отдаем в UI-поток withContext(uiDispatcher) { completed(contentResponse) } } } }<p>Парсинг реализуем с помощью сериализатора типа KSerializer&lt;T&gt;. В нашем случае это NewsList.serializer(). Пропишем реализацию в сервисе новостей:</p>
84 @TheadLocal class NewsService{ companion object { val shared = NewsApi() } val networkService = NetworkService() suspend fun getNewsList(completed: (ContentResponse)-&gt;Unit){ val path = "v2/top-headlines?language=en" networkService.getData(path, NewsList.serializer(),completed) } }<p>Вызывать бизнес-логику будем в презентере. Для полноценной работы с coroutines нам надо будет создать scope:</p>
84 @TheadLocal class NewsService{ companion object { val shared = NewsApi() } val networkService = NetworkService() suspend fun getNewsList(completed: (ContentResponse)-&gt;Unit){ val path = "v2/top-headlines?language=en" networkService.getData(path, NewsList.serializer(),completed) } }<p>Вызывать бизнес-логику будем в презентере. Для полноценной работы с coroutines нам надо будет создать scope:</p>
85 class PresenterCoroutineScope( context: CoroutineContext ) : CoroutineScope { private var onViewDetachJob = Job() override val coroutineContext: CoroutineContext = context + onViewDetachJob fun viewDetached() { onViewDetachJob.cancel() } }<p>и добавить его в презентер. Вынесем в базовый класс:</p>
85 class PresenterCoroutineScope( context: CoroutineContext ) : CoroutineScope { private var onViewDetachJob = Job() override val coroutineContext: CoroutineContext = context + onViewDetachJob fun viewDetached() { onViewDetachJob.cancel() } }<p>и добавить его в презентер. Вынесем в базовый класс:</p>
86 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) } … }<p>Теперь создадим презентер NewsListPresenter для нашего модуля. В инициализатор передадим defaultDispatcher:</p>
86 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) } … }<p>Теперь создадим презентер NewsListPresenter для нашего модуля. В инициализатор передадим defaultDispatcher:</p>
87 class NewsPresenter:BasePresenter(defaultDispatcher){ var service: NewsApi = NewsApi.shared var data: ArrayList = arrayListOf() fun loadData() { //запускаем в скоупе scope.launch { service.getNewsList { val result = it if (result.errorResponse == null) { data = arrayListOf() data.addAll(result.content?.articles ?: arrayListOf()) view?.setupNews(data) } } } } }<p>Обратите внимание! Из-за особенностей<a>текущей работы Kotlin Native с многопоточностью</a>в iOS работа с синглтонами может привести к крашу. Поэтому для корректной работы надо добавить аннотацию @ThreadLocal для используемого объекта:</p>
87 class NewsPresenter:BasePresenter(defaultDispatcher){ var service: NewsApi = NewsApi.shared var data: ArrayList = arrayListOf() fun loadData() { //запускаем в скоупе scope.launch { service.getNewsList { val result = it if (result.errorResponse == null) { data = arrayListOf() data.addAll(result.content?.articles ?: arrayListOf()) view?.setupNews(data) } } } } }<p>Обратите внимание! Из-за особенностей<a>текущей работы Kotlin Native с многопоточностью</a>в iOS работа с синглтонами может привести к крашу. Поэтому для корректной работы надо добавить аннотацию @ThreadLocal для используемого объекта:</p>
88 class NewsService{ @ThreadLocal companion object { val shared = NewsService() } … }<p>Осталось подключить логику к нативным IOS и Android модулям и обработать ответ от Presenter:</p>
88 class NewsService{ @ThreadLocal companion object { val shared = NewsService() } … }<p>Осталось подключить логику к нативным IOS и Android модулям и обработать ответ от Presenter:</p>
89 class NewsListVC: UIViewController { private lazy var presenter: NewsPresenter? = { let _presenter = NewsPresenter() _presenter.attachView(view: self) return _presenter }() override func viewDidLoad() { super.viewDidLoad() self.presenter?.loadData() } extension NewsListVC : INewsListView { func setupNews(list: [NewsItem]) { … }<h4>Android:</h4>
89 class NewsListVC: UIViewController { private lazy var presenter: NewsPresenter? = { let _presenter = NewsPresenter() _presenter.attachView(view: self) return _presenter }() override func viewDidLoad() { super.viewDidLoad() self.presenter?.loadData() } extension NewsListVC : INewsListView { func setupNews(list: [NewsItem]) { … }<h4>Android:</h4>
90 class NewsActivity : AppCompatActivity(), INewsListView { …. private var _presenter: NewsPresenter? = null override fun onCreate(savedInstanceState: Bundle?) { _presenter = NewsPresenter() _presenter?.attachView(this) … } override fun setupNews(list: List) { …. }<p>Запускаем сборку common модуля gradle wrapper, чтобы сборки обновились. Проверяем работу приложений:</p>
90 class NewsActivity : AppCompatActivity(), INewsListView { …. private var _presenter: NewsPresenter? = null override fun onCreate(savedInstanceState: Bundle?) { _presenter = NewsPresenter() _presenter?.attachView(this) … } override fun setupNews(list: List) { …. }<p>Запускаем сборку common модуля gradle wrapper, чтобы сборки обновились. Проверяем работу приложений:</p>
91 <h4>Android</h4>
91 <h4>Android</h4>
92 <h4>iOS</h4>
92 <h4>iOS</h4>
93 <p><strong>Готово. Вы великолепны</strong>.</p>
93 <p><strong>Готово. Вы великолепны</strong>.</p>
94 <p>Оба наши приложения работают и работают одинаково.</p>
94 <p>Оба наши приложения работают и работают одинаково.</p>
95 <p><a>Ссылка</a>на ресурсы.</p>
95 <p><a>Ссылка</a>на ресурсы.</p>
96 <p>Информационные материалы, которые использовались:</p>
96 <p>Информационные материалы, которые использовались:</p>
97 <ul><li><a>туториал от JetBrains</a>;</li>
97 <ul><li><a>туториал от JetBrains</a>;</li>
98 <li><a>https://github.com/JetBrains/kotlin-native</a>.</li>
98 <li><a>https://github.com/JetBrains/kotlin-native</a>.</li>
99 </ul>
99 </ul>