Привет! Это мой первый пост, буду рад услышать профессиональное и не очень мнение по поводу этой статьи. Я мобильный разработчик (таковым себя считаю) с опытом работы около года. В этой статье будет рассмотрено возможное решение проблемы, с которой вы можете столкнуться в процессе освоения Compose Multiplatform. Статья не претендует на истину в последней инстанции и тем более не является прямой инструкцией к выполнению. Вы всегда можете придумать свое, более эффективное и красивое решение, я лишь делюсь собственным опытом разработки.
Результат работы в конце
В одном проекте, который мы решили делать полностью с использованием Compose Multiplatform, была поставлена задача реализовать работу Яндекс Карт. Приложение для сети сервисных центров, поэтому на карте должны отображаться метки СТО, а также собственная метка пользователя для вызова мастера на место.
С самого начала я попробовал проделать стандартную процедуру из документации Kotlin. Подключаем Pod прямо в описании build.gradle, можем даже указать название пакета или еще какие-нибудь флаги cinterop. Под капотом всей этой темы работает cinterop. Он прочтет заголовки уже скомпилированной библиотеки Objective-C и создаст нам klib файлы, которые позволяют легко "трогать" нужный нам функционал, не покидая common module. Кстати, в документации Kotlin даже в качестве примера используется YandexMapsMobile.
Примерная
схема работы с подключенным pod
// build.gradle.kts
kotlin {
ios()
cocoapods {
summary = "CocoaPods test library"
homepage = "https://github.com/JetBrains/kotlin"
ios.deploymentTarget = "13.5"
pod("YandexMapsMobile") {
version = "4.4.0-lite"
}
}
}
// iosMain/*.kt
import cocoapods.YandexMapKit.*
Этот способ не сработал. Конкретно сама библиотека скомпилировалась, линковщик отработал, импорты тоже, даже установка токена работала. Но вот, как только в проекте появился следующий импорт:
import cocoapods.YandexMapsMobile.YMKMapView
То сразу посыпались ошибки линковщика: Undefined symbols:
"OBJC_CLASS на-на-на
Проблема была вызвана именно OpenGL
зависимостью, которую карты используют для отрисовки, именно поэтому
линковщик выдает исключение, когда в проект импортируется представление
карты. Так как проект уже задерживался, нужно в срочном порядке придумать
другое решение.
Можно самостоятельно скомпилировать библиотеку YandexMapsMobile в
framework, включая все зависимости и подключить, что называется, вручную.
Данный способ тоже описан в документации и
довольно широко применяется. Отличие от предыдущего метода заключается лишь
в том, что мы просто собираем framework сами, а не возлагаем эту ношу на
cocoapods.
"А как же мне достать framework, а не pod?" - подумал
я. Оказалось все довольно просто. Получить .framework библиотеки можно
несколькими способами:
После того, как задача с .framework решена, можно приступать к его подключению в проект.
// build.gradle.kts
kotlin {
sourceSets {
val myYandexMapsMobileDefFilePath = "$projectDir/src/nativeInterop/cinterop/YandexMapsMobile.def"
val myYandexMapsMobileCompilerLinkerOpts = "-F${projectDir}/../iosApp/"
val myYandexMapsMobileIncludeDirs = "$projectDir/../iosApp/Pods/YandexMapsMobile"
iosArm64 {
compilations.getByName("main") {
val YandexMapsMobile by cinterops.creating {
// Path to .def file
packageName("cocoapods.YandexMapsMobile")
defFile(myYandexMapsMobileDefFilePath)
includeDirs(myYandexMapsMobileIncludeDirs)
compilerOpts(myYandexMapsMobileCompilerLinkerOpts)
}
}
binaries.all {
// Tell the linker where the framework is located.
linkerOpts(myYandexMapsMobileCompilerLinkerOpts)
}
}
}
}
Как написать .def файл и добавить собственные linker options можете узнать в документации Kotlin multiplatform.
На этом этапе ошибка не пропала, поэтому пришлось решать задачу "обходным путем"
Внимание, если у вас получилось все сделать с помощью предыдущих методов, то это отлично! Решение ниже актуально для меня.
Идея заключается в том, чтобы описать нужные функции представления карты в
протоколе. Через Kotlin framework он передается в Swift (импорт из ComposeApp), там
мы пишем реализацию на Swift (включая UIViewController
) и делаем
инъекцию в DI, тем самым расшарив код для Kotlin. Далее уже в iosMain в Kotlin
нужная реализация достается из DI, дополнительно настраивается и обертывается в UIKitView
.
Общая схема
работы
Представление самой карты написано на swift и используется библиотека, импортированная через pod. Kotlin framework тоже через pod добавлен (native cocoapods), поэтому pod я добавил, просто прописав в Podfile вручную.
# ../iosApp/Podfile
platform :ios, '14.1'
target 'iosApp' do
use_frameworks!
pod 'composeApp', :path => '../composeApp'
pod 'YandexMapsMobile', '4.4.0-lite' # Yandex Maps SDK
end
Для работы с представлением карты в проекте написана Composabel expect функция, она реализована в iosMain и в androidMain. Параметры естественно зависят от ваших потребностей.
@Composable
expect fun YandexMap(
modifier: Modifier = Modifier,
enabled: Boolean = true,
zoom: Float = 14f,
location: LatLng? = null,
startPosition: LatLng? = null,
points: List<PointMapModel>,
onPointClick: (id: Long) -> Unit,
customPosition: Boolean,
canSelectPosition: Boolean,
anotherLocationSelected: Boolean,
bottomFocusAreaPadding: Int,
onPositionSelected: (lat: Double, lng: Double) -> Unit,
onDragged: () -> Unit
)
Следующий код отвечает за реализацию представления с использованием YandexMapsMobile и UIKit. Для удобства можно размещать все классы по разным файлам, как это сделано в примерах, для статьи все занес в один файл.
Ниже описана функция для инициализации DI. Реализация карты передается через параметр и встраивается вместе с общим модулем и специфичным для платформы.
fun initKoinIos(
mapProtocol: YandexMapProtocol
) {
startKoin {
modules(
module {
single<YandexMapProtocol> { mapProtocol }
} +
commonModule() +
listOf(platformModule())
)
}
Napier.base(DebugAntilog())
}
Собственно сам запуск DI и также установка токена карты. Запускать карту onStart()
в
этом месте вовсе необязательно.
import SwiftUI
import ComposeApp
import YandexMapsMobile
@main
struct iOSApp: App {
init() {
YMKMapKit.setApiKey(Constants().MAPKIT_API_KEY)
YMKMapKit.sharedInstance().onStart()
KoinDI_iosKt.doInitKoinIos(mapProtocol: YandexMapProtocolImpl())
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Реализация actual функции со специфичной логикой.
Главные моменты это изъятие из DI нужного модуля.
val yandexMapProtocol = koinInject<YandexMapProtocol>().apply {
addCameraListener(onDragged)
addMapPointListener(onPointClick)
}
Далее обертка в UIKitView
UIKitView(
factory = {
yandexMapProtocol.viewController.view
},
Полный код файла:
Еще раз повторюсь, что описанное решение не является самым оптимальным путем. Так как лучше всего описывать весь интерфейс на Kotlin, но это только, если удастся победить cinterop. А данной реализации нам хватило, скорее всего в дальнейшем мы напишем полный протокол и заведем это в отдельное SDK для внутреннего использования, чтобы подключать как модуль.
Быстродействие карты такое же как и в стандартной реализации, все-таки это native 🤙.
Доклад про работу c нативными зависимостями в KMP проекте
(про framework, cinterop)
Статья про реализацию Yandex
Maps в pure iOS
Про вызов специфичных для платформы
модулей
Добавление iOS зависимостей в KMP проект
Некоторые
membres only, но никто вам не запрещает глянуть зеркала
Спасибо за прочтение статьи! Когда я сам искал решение этой проблемы, мне было довольно сложно, не хватало поддержки от опытных разработчиков, поэтому, надеюсь, что мой материал все-таки кому-нибудь пригодится. Видео работы приложения приведено ниже. Комментарии и критика приветствуется, но учтите, мне 17, если обидите, то маме пожалуюсь 😉.
Если вам понравилось статья и хотите меня отблагодарить, то можете подписаться на мой TG канал https://t.me/stakan_live. Маленький блог о разработке и жизни.
Теги:
Хабы: