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 () -> Unit) { GlobalScope.launch(MainDispatcher) { block() } }<h4>Android</h4>
81
actual fun ktorScope(block: suspend () -> Unit) { GlobalScope.launch(MainDispatcher) { block() } }<h4>Android</h4>
82
actual fun ktorScope(block: suspend () -> Unit) { GlobalScope.launch(Dispatchers.Main) { block() } }<p>Применим это при реализации сетевого клиента на Ktor:</p>
82
actual fun ktorScope(block: suspend () -> Unit) { GlobalScope.launch(Dispatchers.Main) { block() } }<p>Применим это при реализации сетевого клиента на Ktor:</p>
83
interface INetworkService { suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)->Unit) } class NetworkService : INetworkService{ private val httpClient = HttpClient() override suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)->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<T>. В нашем случае это NewsList.serializer(). Пропишем реализацию в сервисе новостей:</p>
83
interface INetworkService { suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)->Unit) } class NetworkService : INetworkService{ private val httpClient = HttpClient() override suspend fun getData(path: String, serializer: KSerializer,completed: (ContentResponse)->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<T>. В нашем случае это NewsList.serializer(). Пропишем реализацию в сервисе новостей:</p>
84
@TheadLocal class NewsService{ companion object { val shared = NewsApi() } val networkService = NetworkService() suspend fun getNewsList(completed: (ContentResponse)->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)->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>