Привет! Меня зовут Мялкин Максим, я занимаюсь мобильной разработкой в KTS.
Мы в мобильной команде для шаринга кода на несколько платформ используем KMM.
На Хабре можно встретить достаточное количество статей по этой технологии, но большинство из них рассматривает либо выбор кроссплатформенной технологии, либо перевод проекта на KMM.
Я расскажу наш опыт взаимодействия с KMM со стороны iOS-разработки: с какими проблемами столкнулись, их решение, наш подход и главное — как к этой технологии относятся iOS-разработчики.
Содержание:
Контекст
Kotlin Multiplatform Mobile (KMM) — это SDK для мультиплатформенной разработки от компании JetBrains. KMM позволяет вынести переиспользуемую бизнес-логику в общий модуль для платформ iOS и Android.
В августе 2020 выпущена альфа-версия KMM. Недавно технология вышла в бету. При этом Google начал перенос библиотек Jetpack на KMM.
Сейчас появляется всё больше кейсов использования КММ в мобильных приложениях в крупных компаниях:
Мы в команде используем КММ для оптимизации разработки и поддержки существующего кода, что особенно важно на проекте с ограниченными сроками. В причины выбора углубляться не буду, но если вкратце — КММ позволил не переобучать ребят, как это было бы с Flutter.
Бизнес-логика и работа с данными обычно идентичны для Android и iOS. А КММ позволяет написать код сразу для двух платформ и при этом оставить реализацию UI нативной.
Мы выносим в модуль KMM всю независимую от платформы логику:
запросы в сеть;
парсинг данных;
хранение данных;
бизнес-логика: проверка авторизации, валидация введённых данных, изменение состояния экранов. Бизнес-логика представлена у нас в качестве MVI-фичи, написанной с использованием MVIKoltin. Об этом ещё напишет в одной из следующих статей наш iOS-разработчик.
Что выносят в кроссплатформенную часть другие компании, можно посмотреть в результатах опроса jetbrains.
Android-разработка с использованием КММ никак не меняется, за исключением библиотек работы с сетью и хранения данных. Многомодульные проекты уже стали стандартом в Android-разработке. А бизнес-логика пишется на чистом Kotlin без платформенных зависимостей в соотвествии с чистой архитектурой.
Но для iOS-разработки при внедрении KMM есть нюансы, о которых мы поговорим дальше.
Kotlin
Не все iOS-разработчики реализовывают общую KMM-часть функциональности, но у нас в команде этим занимаются не только Android, но и iOS-разработчики.
Первая проблема, с которой сталкивается iOS-разработчик — новый язык. Большинство разработчиков не работали с Kotlin. Но при этом все работают со Swift. В нашей команде и по отзывам других компаний у iOS-разработчиков не возникло трудностей с пониманием Кotlin-кода. Kotlin и Swift являются современными и развивающимися языками программирования, очень многое в них похоже.
Что было непривычно в начале:
нельзя использовать одинаковые имена методов, классов внутри одинаковых пакетов, даже если они объявлены в разных частях: common, iOS;
тяжело читаются конструкции let и другие scope functions, expression chain.
Также нюансы скрываются на границе интеропа Swift-Kotlin. Для Android используется Kotlin/JVM, а для iOS — Kotlin/Native.
В большинстве случаев эти нюансы незначительны:
в Kotlin нет checked-исключений, как в Swift. Если метод бросает исключение, то в iOS-части будет крэш. Чтобы этого не было, необходимо у объявления метода указать аннотацию @Throws(
Exception::class
). Но мы придерживаемся подхода возвращения обертки Result, то есть метод возвращает либо success, либо fail, и исключение не бросается никогда;Еxtensions в Kotlin в большинстве случаев не преобразовываются в Swift-extensions;
иногда внутренние классы из Kotlin преобразуются во внутренние классы Swift, а иногда нет;
отсутствие поддержки generic protocol (это достаточно важный пункт при работе с Kotlin Flow), generic func;
отсутствие поддержки дефолтных аргументов в Swift;
отсутствие поддержки sealed class в Swift.
Часть этих нюансов можно исправить с помощью gradle plugin, который будет генерировать более подходящий Swift-код. Для улучшенной поддержки корутин, Flow в Swift можно использовать библиотеку.
Список интероп-особенностей можно найти в статье от Jetbrains. Ещё есть репозиторий от команды HH с подробным описанием и объяснением всех нюансов интеропа и примерами использования.
Что вызывает проблемы:
обновление версии Kotlin нужно производить аккуратно и тестировать всё приложение. У нас бывали случаи конфликтов зависимостей, которые приводили к крашам в iOS и Android-рантаймах;
необходимо учитывать нюансы работы с памятью в многопоточной среде в Kotlin Native. При передаче между потоками объекты должны быть иммутабельными. Эта проблема встречается практически сразу, когда вы пытаетесь отобразить данные из сервера на экране (хотя и не мутируете их) при использовании связки ktor + kotlinx.serialization. На Github есть issue с хаком для обхода проблемы.
Сейчас проблемы иммутабельности уходят с выходом новой модели памяти. Она включена по умолчанию, начиная с версии 1.7.20. Теперь доступ к объектам доступен из любых потоков и используется трассирующий сборщик мусора.
Важно понимать, что проблемы возможны, потому что технология не в релизе. По имеющейся информации, релиз планируется на конец 2023 года.
Окружение
В настоящий момент для работы с KMM и iOS мы используем 2 среды: Android Studio и Xcode.
Одной лишь Android Studio при разработке недостаточно: она не позволяет нормально работать с iOS-кодом. Подсветка синтаксиса, компиляция и запуск приложения работают (как это устроено под капотом, можно посмотреть в интервью с разработчиком плагина KMM), но навигация, подсказки, поиск использований — нет. В целом для iOS-разработчика пользоваться Android Studio приятно: удобная отладка, работа с терминалом и Git. Но она довольно требовательна к ресурсам.
Из-за ограничений студии разработчику нужно держать открытыми 2 среды — Android Studio и Xcode, а это повышает требования к машине разработчика. При этом много памяти съедает и система сборки Gradle. Но с 16Gb ОП вполне можно комфортно пользоваться сразу 2 IDE — Xcode и Android Studio на небольших проектах.
Скрины использования 2-х систем на разных машинах
Для решения мы пробовали использовать AppCode вместо двух IDE. В нём всё сразу из коробки, он понятный, если до этого имел дело с Android Studio. Но при этом он платный, и, к сожалению, недавно Jetbrains заявили, что он прекращает развитие.
На сегодня мы видим оптимальным параллельное использование Xcode и Android Studio, если позволяют ресурсы машины.
Чтобы удостовериться, что на машине разработчика установлено всё необходимое ПО, используйте KDoctor.
Мы встречали проблемы со сборкой KMM-части на Mac с Apple silicon. Помогли решения из этой статьи. Изначально мы работали с Rosetta, что увеличивало время сборки, но с версии Kotlin 1.5.30 поддерживаются чипы Apple silicon.
Нюансы с использованием KMM
Связь common кода с iOS проектом
Работая с КММ в iOS, сразу возникает вопрос — как подключить модуль с shared кодом в проект?
Сейчас есть 2 способа:
Cocoapods
Regular framework
При использовании regular framework в iOS-проекте добавляется вызов скрипта перед сборкой:
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode
После этого вышла интеграция с cocoapods, и мы начали использовать её (мы используем на iOS этот dependency manager), избавившись от лишних шагов.
Под капотом плагин по умолчанию автоматически генерирует файл podspec для shared-библиотеки.
Внутри podspec добавляется script phase, которая позволяет при каждой сборке iOS-приложения собирать shared-модуль и видеть все изменения в нем.
shared.podspec
spec.script_phases = [
{
:name => 'Build shared',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$COCOAPODS_SKIP_KOTLIN_BUILD" ]; then
echo "Skipping Gradle build task invocation due to COCOAPODS_SKIP_KOTLIN_BUILD environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
SCRIPT
}
]
Нужно отметить, что сейчас вам не нужно ничего настраивать вручную для связи shared c iOS-частью. При создании проекта всё уже настроено и работает стабильно.
Раньше требовалась ручная настройка, и появлялись случаи, что iOS-проект не собирается, потому что не видит новых изменений в shared-модуле.
Где хранить common-код?
Также в проекте вы можете использовать монорепозиторий для кроссплатформенного и нативного кода...
...либо распространять кроссплатформенную часть независимо.
Мы используем монорепозиторий, что позволяет писать кроссплатформенный код разными разработчиками (как iOS, так и Android) и сразу же интегрировать изменения в нативную часть без промежуточной публикации артефакта.
Coroutines, Flow
iOS-разработчик достаточно быстро может разобраться с использованием корутин в Kotlin. В Swift 5.5 добавлен асинхронный подход с помощью async-await и structured concurrency (о котором мы делали перевод). Это делает асинхронность в Swift и Kotlin схожими. То есть iOS-разработчик может без особого труда писать асинхронный код в shared-части, особенно если проект уже засетаплен и подходы написания кода в проекте определены.
Нюансы возникают при интеропе Kotlin-Swift. Вызов suspend
-метода в Kotlin по умолчанию превращается в completionHandler
в Swift.
Также необходимо навешивать на suspend
-методы аннотацию @Throws, чтобы оповестить Swift о возможной ошибке, потому что в Kotlin нет checked-исключений. Без аннотации при возникновении ошибки в suspend-методе приложение будет крашиться.
Помимо completionHandler
для suspend
-методов можно использовать async-await синтаксис. В настоящий момент эта фича находится в экспериментальном статусе и имеет ограничения structured-concurrency.
Как completionHandler
, так и async-await не поддерживают отмену корутин. KMP-NativeCoroutines позволяет исправить этот недостаток.
Мы в проектах не используем вызов suspend-методов из Swift, потому что взаимодействие с shared ограничивается интерфейсом MVI-Store, в который мы прокидываем интенты и наблюдаем за изменением состояния экрана, грубо говоря, через колбек. А вся работа с асинхронностью происходит внутри MVI только в Kotlin-части.
Краткая реализация MVI
// ios common
class MviComponent {
…
fun onStart() {
binder = bind(mainContext = Dispatchers.Main.immediate) {
store.states bindTo ::acceptState
}
}
private fun acceptState(state: StateType) {
mviView.render(state)
}
}
// ios native
final class FeatureView: MviView {
override func render(model: ClaimDetailsStoreState) {
// отправка значения в VC
}
}
Нативные библиотеки в common
Иногда нужно использовать нативную функциональность платформы и обратиться к нему из common-части.
В большинстве случаев хватает механизма expect/actual. В таком случае вы внутри actual-реализации можете использовать нативные библиотеки iOS. Например, хранилище key-value на Android реализуется с помощью SharedPreferences, а на iOS с помощью UserDefaults. В таком случае у вас в common будет расположен expect class KeyValueStorage
Кроме expect/actual-механизма можно использовать интерфейсы с реализацией, где реализация проставляется в DI внутри платформы.
Пример с интерфейсами
//common
interface CookieStorage {
suspend fun getCookiesForUrl(link: String): List<Cookie>
suspend fun clearCookie()
}
//iOS common implementation
class CookieStorageImpl : CookieStorage {
override suspend fun getCookiesForUrl(link: String): List<Cookie> {
NSHTTPCookieStorage.sharedHTTPCookieStorage()
…
}
override suspend fun clearCookie() {
val cookieStore = NSHTTPCookieStorage.sharedHTTPCookieStorage()
….
}
}
//iOS common di
val authPlatformModule = module {
single<CookieStorage> {
CookieStorageImpl()
}
}
Кстати, этот пример можно реализовать с помощью KMM реализации Datastore от Google.
Ещё один пример, как реализовать взаимодействие с платформой в common-части: прокидывать closure в KMM-часть из нативной. Хотя это выглядит как костыль (приходится использовать глобальные переменные и методы) и иногда этого можно избежать.
Пример с closure
//iOS common
internal actual fun provideErrorReporters(): List<ErrorReporter> {
return iOSReportersClosure()
}
internal var iOSReportersClosure: (() -> List<ErrorReporter>) = {
emptyList()
}
class iOSDi {
fun setIosReporters(closure: (() -> List<ErrorReporter>)) {
iOSReportersClosure = closure
}
}
// iOS native
iOSDi().setIosReporters(closure: {
return [IosErrorReporter()]
})
Мы почти всегда используем подход с интерфейсом и платформенными реализациями.
Common-библиотеки в нативе
По умолчанию в нативе вы можете использовать всё, что написали в common-модуле и что имеет public-видимость.
Но доступ к коду библиотек, которые вы подключили в common, будет неполный.
Чтобы библиотеки из common можно было использовать в нативной iOS-части, необходимо добавить export:
cocoapods {
framework {
export(Deps.Kts.Auth.coreAuth)
export(Deps.KmmColors.core)
}
}
До начала разработки на КММ в KTS Android-команда наработала библиотеки, которые нам удалось разделить на нативную и Kotlin-часть, а затем переиспользовать Kotlin-часть в KMM.
Реализация конкретных областей проекта
В этой части статьи мы рассмотрим основные аспекты любого проекта, как можно подойти к их решению с помощью КММ и какие библиотеки существуют.
От разработчика Jetbrains в открытом доступе есть список совместимых с KMM библиотек: https://github.com/terrakok/kmm-awesome. Внутри большое количество библиотек, присутствует разделение по разделам, так что ориентироваться там просто.
DI
Мы используем Koin
в качестве DI в KMM части. Это самая популярная библиотека, поддерживающая Kotlin, и у нас с ней был опыт на Android, который позволяет достаточно просто интегрироваться.
Но в нативной части iOS мы не можем использовать её в качестве DI, поэтому в iOS используем Swinject
. Внутри Swinject
используется связка VC
, MVI Store
и других сущностей, которые находятся только в iOS-части и никак не передаются в common. Сам MVI Store
создаётся в модулях Koin
.
Так у нас получается 2 несвязанных графа зависимостей: Swinject
и Koin
. Чтобы их подружить, мы используем прослойку. Выглядит это следующим образом.
В части common-iOS добавляем класс для фичи с названием Feature
:
//shared/src/iOSMain/kotlin/org/example/di/FeatureDi.kt
class FeatureDi : KoinComponent {
fun featureStore(param: Parameter): FeatureStore = get {
parametersOf(param)
}
}
В нативной iOS-части:
final class FeatureAssembly: Assembly {
func assemble(container: Container) {
container.register(FeatureViewController.self) { (resolver) in
let store = FeatureDi.featureStore()
return FeatureViewController(store: store)
}
}
}
Таким образом, методы из KMM FeatureDi
вызываются только внутри Assembly
.
Если требуется зависимость в KMM-части, привязанная к скоупу (аналог Swinject custom scope), то скоуп создаётся в koin.
При необходимости можно сказать из нативной части, в какой момент закрыть скоуп:
class FeatureDi : KoinComponent {
fun closeFeatureFlow() = getScope<FeatureScope>().close()
}
Такая прослойка позволяет сделать 2 DI фреймворка независимыми друг от друга.
Конечно, в идеальном мире должен быть 1 фреймворк для DI без каких-либо прослоек. Который будет поддерживать как нативные зависимости, так и кроссплатформенные. Так работает на Android с Koin. Но для iOS я таких реализаций пока не видел. Если вы знаете о таких — напишите в комментариях ????
Навигация
По этой части на текущий момент для KMM нет никаких готовых решений, которые будут поддерживать навигацию в iOS и Android.
Что есть из решений:
Odyssey и Voyager поддерживают навигацию в compose multiplatform, не работают с iOS.
Decompose позволяет реализовать подход разбиения функциональности на компоненты с учетом ЖЦ и добавить навигацию.
Нам эти решения не подошли, потому что в проектах нужно поддерживать навигацию с учетом разных реализаций платформенного UI (Fragments/UIKit/Compose/SwiftUI), а соответственно, и подходов навигации. При этом нам не хотелось бы на текущий момент кардинально менять парадигму создания функциональности, как это пришлось бы делать с Decompose.
Поэтому мы сейчас используем нативную навигацию. В iOS это сделано через Coordinator.
Кто ответственен за логику навигации?
В большинстве случаев навигация простая: перейти между экранами по заранее фиксированным правилам. В таких случаях мы из бизнес-логики (Store
) оповещаем, что произошло событие X (например, создана заявка). В платформенной части, если пришло событие X — осуществляем навигацию на заданный экран. Если заявка создана — возвращаемся на экран списка всех заявок.
Но бывает и более сложная логика: например, флоу экранов, переходы внутри которого управляются бизнес-логикой, и с каждого экрана можно перейти на любой другой.
В таком случае применить правило выше не получится: придётся создавать много разных событий и реагировать на все в каждом экране внутри flow.
Возможные события навигации
Screen1
navigateToScreen2
navigateToScreen4
Screen2
navigateToScreen3
navigateToScreen4
Screen3
navigateToScreen2
navigateToScreen4
Screen4
navigateToScreen1
navigateToScreen3
navigateToScreen5
В таких случаях мы создаем event NavigateTo(screen)
, принимая который, платформенная часть переходит на screen, ничего дополнительно не вычисляя. А стейт-машина с логикой переходов находится в общей части.
Network
По работе с сетью с библиотеками все ок.
Есть Ktor, который позволяет кроссплатформенно совершать запросы. В нём можно настроить всю необходимую функциональность. Под капотом используется нативный engine для выполнения запросов, например URLSession
.
Если у вас GraphQL
, то для чистого Kotlin существует клиент Apollo. Он без проблем работает в KMM.
При работе с сетью у нас возникала проблема отправки файлов через Ktor: как отправить файл на сервер, при этом не выгружая его полностью в память, а используя потоки (streams).
В Ktor есть возможность отправлять поля формы с помощью специального класса — Input. Он абстрагирован от содержимого. В Android есть возможность получить InputStream
из файла и преобразовать его в Input
с помощью extension. Так при отправке файла будут вычитываться байты из файлового InputStream
, и в памяти файл сохраняться не будет.
На iOS возможности получить Input
для файла (файл получается из URL) из коробки нет и нужно писать логику передачи самому.
Также можно использовать ByteArray
для отправки файла, но тогда весь файл у вас кладётся в память.
Пример реализации для ByteArray
let data = Data(contentsOf: url)
let byteArray = data.toArray(type: UInt8.self)
let kotlinByteArray = KotlinByteArray.init(size: Int32(byteArray.count))
let intArray: [Int8] = byteArray.map {
Int8(bitPattern: $0)
}
for (index, element) in intArray.enumerated() {
kotlinByteArray.set(index: Int32(index), value: element)
}
Мы использовали такой вариант, т.к. он подходил нам под задачу (можно было выбирать файлы только маленького размера).
UI
На стороне UI KMM пока нечего предложить, но он и не целился в эту сторону активно. Есть compose multiplatform, который стабильно работает на android и desktop, но на iOS только в превью. Пример работы с ним можно посмотреть тут.
Пока до стабильности compose multiplatform iOS далеко, поэтому мы используем нативный UI: SwiftUI, UIKit
.
Но всё-таки в КММ можно шарить кое-что в презентационном слое (мы для этого используем MOKO-Resources):
строки с переводами
цвета
изображения — по дефолту только растровые. Можно использовать обходные пути, библиотека kmm-images
шрифты
Под капотом библиотека при компиляции проекта генерирует из КММ нативные ресурсы для каждой из платформ.
Кроме этого, между платформами мы переиспользовали темы приложения таким образом, что при смене ночного режима тема автоматически меняется. Про эту реализацию мы планируем написать статью.
Отладка приложения
При отладке приложения всплывает проблема нескольких IDE. Можно либо дебажить с помощью Xcode c использованием плагина xcode-kotlin, либо с использованием AndroidStudio, AppCode. Наши разработчики используют оба подхода в зависимости от личных предпочтений.
Краши
При возникновении краша в КММ-части всегда ошибка указывает на assembly code. Но для разбора причин ошибки помогает стектрейс потока. По нему можно приблизительно узнать, где произошла ошибка. Так же работает переход на КММ-файл при нажатии на фрейм стектрейса. Но точное место ошибки не подсвечивается.
В крашлитике также возможны проблемы с отображением логов.
Для получения более подробной информации можно использовать CrashKit.
Логирование некритичных ошибок
Логирование некритичных ошибок KMM почти неизменно относительно нативной разработки.
Создаём интерфейс ErrorReporter, который сможет логировать ошибки в сервис:
interface ErrorReporter {
fun setUserId(userId: String)
fun logError(throwable: Throwable)
}
Создаём композитный репортер, чтобы уметь отправлять ошибки в разные сервисы:
class CompositeErrorReporter(
private val reporterList: List<ErrorReporter>
) : ErrorReporter {
override fun setUserId(userId: String) {
reporterList.forEach { it.setUserId(userId) }
}
override fun logError(throwable: Throwable) {
reporterList.forEach { it.logError(throwable) }
}
}
Предоставляем платформенные логгеры:
//Common
internal expect fun provideErrorReporters(): List<ErrorReporter>
//DI
factory<ErrorReporter> {
CompositeErrorReporter(
reporterList = provideErrorReporters()
)
}
//Common iOS
internal actual fun provideErrorReporters(): List<ErrorReporter> {
return iosReportersClosure()
}
internal var iosReportersClosure: (() -> List<ErrorReporter>) = { emptyList() }
//Native iOS
@objc class CrashlyticsIosErrorReporter: NSObject, ErrorReporter {
func logError(throwable: KotlinThrowable) {
// send throwable to crashlytics
}
}
iOSDi().setIosReporters(closure: {
return [IosErrorReporter()]
})
Потенциально нативный репортер можно переместить в common ios.
В common cоздаем связь между логером и репортером. Мы в качестве логера используем Napier.
class ErrorReporterAntilog(
// в качестве ErrorReporter подставляется CompositeErrorReporter
private val errorReporter: ErrorReporter
) : Antilog() {
override fun performLog(...) {
errorReporter.logError(exception)
}
}
//при запуске приложения в iOS native
Napier.releaseBuild(antilog: ErrorReporterAntilog…)
После этого логируем ошибку в KMM, и она отправляется на обеих платформах, например в крашлитику.
Napier.e("Error", throwable)
Утечки памяти
Один из пунктов, с которыми нужно иметь дело при разработке приложения — убедиться, что утечек памяти нет. В iOS с этим непросто и без КММ: отсутствуют инструменты, позволяющие своевременно заметить утечку памяти.
Необходимо использовать Memory Graph Debugger, чтобы периодически проверять, что ничего не утекло. Либо писать тесты и запускать их на CI. На Android есть библиотека leakcanary, которая позволяет проверять базовые утечки, связанные с фреймворком. К сожалению, похожего решения на iOS нет.
С использованием KMM в iOS мы можем прибегать к тем же техникам отслеживания утечек, что и в нативе:
В какой-то момент понимаем, что что-то идёт не так ????:
Открываем Memory Graph Debugger:
Смотрим дополнительную информацию об утекающем объекте:
Как вы можете заметить из картинки выше — ничто в ней не указывает, где случилась утечка. И по нашему опыту в утечках всегда присутствуют кишки библиотек. Поэтому поиск проблемы может занимать длительное время.
Процесс
Для описания процесса рассмотрим проект с 4 мобильными разработчиками: 2 android / 2 iOS. iOS-разработчики не имели опыта в KMM до этого.
Сначала на проекте Android-команда разрабатывала КММ-фичи, а iOS-команда подключала их к себе уже готовые, накручивая только UI-часть.
Со временем iOS-разработчики подключились к код-ревью КММ-части в режиме наблюдения. Спустя несколько месяцев iOS-разработчики уже реализовывали внутри KMM шаблонные фичи и правки логики. Спустя еще пару месяцев iOS-разработка дошла до полноценной разработки в KMM.
В ноябре у части Android-команды был отпуск, поэтому iOS-разработчики делали бОльшую часть КММ задач.
При рассмотрении графика необходимо понимать, что у нас в команде не стояло цели как можно быстрее научить iOS-разработку реализации KMM. С учетом существующего проекта с принятыми и устоявшимися подходами это можно сделать и быстрее.
Также мы прошли путь изменения разделения реализации фичи на независимые части:
-
Изначально на фичу ставилось 3 задачи: KMM, Android, iOS. Изначально выполнялась задача KMM, потом 2 задачи платформы. Этот подход плохо работал для нас: в момент реализации нативной задачи (например Android) оказывалось, что в КММ мы что-то упустили и требуется доработать КММ-модуль, а переделки влияли и на другую платформу, которая к тому моменту могла бы быть в процессе, и её приходилось перерабатывать.
Статистику количества таких задач мы не собирали, но по ощущению, их было около 60-70%. И каждый разработчик подмечал эту проблему на ретро.
Сейчас реализация задачи делится на 2 части: KMM + первая платформа, вторая платформа. То есть в начале разрабатывается КММ в паре с платформенной частью, например Android, если Android-команда первой подошла к реализации функциональности. После этого разрабатывается вторая платформенная часть, iOS. Так переделки в КММ удалось сократить. Хотя они всё-таки случаются, когда одной из платформ API оказывается неподходящим.
Еще хотелось обратить внимание на один нюанс: т.к. мы используем монорепозиторий, любой разработчик может в своей задаче затронуть КММ-часть. А изменения в ней могут поломать платформы.
Пример. Android-разработчик поменял КММ, из-за этого сломалась iOS-часть, а разработчик этого мог даже не увидеть, потому что никогда не собирает iOS-проект у себя локально. Это можно контролировать тестами, но минимально достаточно на CI перед закрытием merge request добавить шаг «автосборка IOS и Android проектов».
Чтобы ребята, реализующие KMM, понимали, как он интегрируется с платформами, в которых они не разбираются, мы провели сессии с объяснением базовых компонентов и их связью в Android и iOS. Еще эти сессии помогли разработчикам соседних платформ лучше локализовать ошибки и даже исправлять не очень сложные ошибки при изменении в KMM.
Теоретически, не у всех Android-разработчиков может быть рабочая машина с MacOS. В этом случае для интеграции KMM с iOS и правками понадобится помощь iOS-разработчиков.
Итог
Статус технологии
В KMM есть свои нюансы, которые мы обсудили в статье. Но за время разработки мы не встретили блокеров, которые заставили задуматься об отказе от KMM и вернуться в натив/перейти на другую кроссплатформенную технологию.
Личное впечатление
iOS-разработчики приняли KMM хорошо: у нас в команде у всех есть желание писать кроссплатформенную часть. Но при этом я уверен, что это не будет так для всех разработчиков в общем случае.
Команды iOS и Android стали более сплоченными: теперь обсуждаются не только общие концепции разработки, но и общая реализация.
Бизнес
В целом статья не направлена на обоснование КММ бизнесу, но без этих выводов выглядит неполной.
-
Экономия времени разработки: зависит от проекта.
В наших текущих проектах идеальное сокращение времени составляет примерно 25%. Соотношение времени реализации бизнес-логики к UI 50-50. Значит, мы сокращаем реализацию бизнес-логики на одной из платформ. Реальное меньше: разработчикам нужно погрузиться в новую технологию, изредка случаются доработки КММ-части в зависимости от платформ и инфраструктурные сложности.
На сколько меньше точно, оценить сложно: для этого нужно сравнивать натив vs KMM на одинаковых проектах. Экономия будет больше, если в проектах много бизнес-логики находится на мобильном клиенте: офлайн-first решения, примеры таких мы рассматривали на vc (ПИК, Ascott).
Экономия времени переделок при изменении только бэкенда/логики без изменения UI — почти 50%.
Время погружения разработчиков iOS в KMM с 0 до реализации комплексных фичей — около 4 месяцев. При том, что у нас в команде не стояло цели как можно быстрее научить iOS-разработку реализации KMM. С учетом существующего проекта с принятыми и устоявшимися подходами, это можно сделать быстрее
Bus factor увеличивается — доработать KMM-логику могут не только iOS-разработчики, но и Android, если у iOS не хватает времени, они заняты на другом проекте или ушли в отпуск
Нам удалось перейти на кроссплатформу без дополнительного найма людей и переучивания
Наш подход разработки в KMM-части не сильно отличается от нативного Android, который мы обкатывали давно. Что достаточно ускоряет погружение в KMM нового человека в команде: недавно к нам вышел новый Android-разработчик, и менее чем через неделю уже мог дорабатывать KMM часть. Часть из этого времени было потрачено на погружение в MVI-подход
Поделитесь в комментариях своим опытом внедрения KMM в команду мобильной технологии. Как iOS-разработчики задействованы в кроссплатформенной разработке?
Курс Android на Kotlin????
20 марта стартует новый поток курса «Android-разработчик».
Перед курсом мы провели два обучающих вебинара:
Первый вебинар — «Model-View-паттерны презентационного слоя», запись можно посмотреть по ссылке: https://youtu.be/aTSnt9qtrHY
Второй вебинар — «Использование паттерна MVI в KMM», запись можно посмотреть по ссылке: https://youtu.be/g1TtM-bUmR4
Занятия на курсе проводят наши сотрудники. Команда разработала приложения для таких компаний, как ПИК и GeekBrains. Поэтому на курсах вы получите актуальные знания, которые требуются на сегодняшнем рынке мобильной разработки.
???? Посмотреть полную программу курса и записаться можно здесь: https://vk.cc/cln4GI
???? Старт — 20 марта.
PS ???? Вы можете получить другие интересные материалы по Android-разработке
в нашем боте
Комментарии (10)
Sigest
00.00.0000 00:00Благодарю за статью. Сам сейчас изучаю КММ и информации довольно мало Относительно нативных iOs, Android. Скажите пожалуйста, А есть ли советы по литературе, что почитать? Ваша команда откуда черпает информацию?
MAX1993M Автор
00.00.0000 00:00+1Как такового универсального источника нет. В зависимости от запроса ищется ответ в разных местах.
Можно смотреть:
официальная дока и подраздел multiplatform - постоянно добавляются новые разделы
сайт icerock - база знаний с решениями проблем и примерами работы с их библиотеками
документация по библиотекам на сайтах и гитхабе (например ktor), к исходникам периодически нужно обращаться
на хабре есть по кмм попадаются полезные статьи
visirok
00.00.0000 00:00Спасибо за очень основательную статью!
Я пишу мультиплатформенную библиотеку. Хотелось бы покрыть её тестами на конкретных платформах. Понятно, как это сделать для JVM и JS. Не подскажете, как лучше ее тестировать на остальных платформах? Не хотелось бы ради этого городить маленькие Аппы. Хотелось бы что-то вроде JUnit.
MAX1993M Автор
00.00.0000 00:00+1Смотря какие тесты нужны. Использовать юнит-тесты можно. На вашем месте я бы смотрел примеры крупных библиотек и на их тесты. Тесты для разных платформ можно найти в кторе. А тут можно найти список библиотек по разделам, изучить библиотеки похожие на вашу и их тесты.
tawfik
00.00.0000 00:00Прочел статью, и как-то подумал стоит ли перейти на KMM и дальше писать велосипеды чтобы решать проблемы, которые нативно можно просто решить ? ????
almazrafi
00.00.0000 00:00+3Сейчас появляется всё больше кейсов использования КММ в мобильных приложениях в крупных компаниях:
- QIWI
- HH
Здесь "HH" - это hh.ru?
MAX1993M Автор
00.00.0000 00:00+1Да.
Изначально я в фоне накидывал список компаний, которые так или иначе связаны с КММ.
Перед публикацией статьи добавлял ссылки на открытые источники с подтверждением информации.
Компании, по которым не удалось найти открытых данных удалялись.
У хх был в качестве источника указан их репо с интеропом, что прямо не говорит об использовании внутри компании. Но этот момент был упущен при публикации статьи. Так что я удалил эту информацию сейчас.
Спасибо за заметку.
nikita_dol
Почему решили не использовать FileHandle для чтения файла частями?
MAX1993M Автор
Его использовать можно, но нужно интегрировать с классом Input. Для нашей задачи подошел ByteArray. Возможно, интеграция готовая уже есть.
Поискал еще раз - нашел упоминание у icerock.
Завести ее не получилось, тк используется экспериментальное апи из старой версии ктора . В актуальной версии аналога найти не удалось.