Pull to refresh
89.16
KTS
Создаем цифровые продукты для бизнеса

KMP глазами iOS-разработчика

Level of difficultyMedium
Reading time18 min
Views10K

Привет! Меня зовут Мялкин Максим, я занимаюсь мобильной разработкой в KTS.

Мы в мобильной команде для шаринга кода на несколько платформ используем KMP. 

На Хабре можно встретить достаточное количество статей по этой технологии, но большинство из них рассматривает либо выбор кроссплатформенной технологии, либо перевод проекта на KMP.

Я расскажу наш опыт взаимодействия с KMP со стороны iOS-разработки: с какими проблемами столкнулись, их решение, наш подход и главное — как к этой технологии относятся iOS-разработчики.

Содержание:

Контекст

Kotlin Multiplatform (KMP) — это SDK для мультиплатформенной разработки от компании JetBrains. КМP позволяет вынести переиспользуемую бизнес-логику в общий модуль для платформ iOS и Android. 

В августе 2020 выпущена альфа-версия KMP. Недавно технология вышла в stable. При этом Google начал перенос библиотек Jetpack на KMP.

Сейчас появляется  всё больше кейсов использования КМP в мобильных приложениях в крупных компаниях:

Мы в команде используем КМP для оптимизации разработки и поддержки существующего кода, что особенно важно на проекте с ограниченными сроками. В причины выбора углубляться не буду, но если вкратце — КМP позволил не переобучать ребят, как это было бы с Flutter.

Бизнес-логика и работа с данными обычно идентичны для Android и iOS. А КМP позволяет написать код сразу для двух платформ и при этом оставить реализацию UI нативной. 

Мы выносим в модуль КМP всю независимую от платформы логику: 

  • запросы в сеть;

  • парсинг данных;

  • хранение данных;

  • бизнес-логика: проверка авторизации, валидация введённых данных, изменение состояния экранов. Бизнес-логика представлена у нас в качестве MVI-фичи, написанной с использованием MVIKoltin. Об этом ещё напишет в одной из следующих статей наш iOS-разработчик.

Что выносят в кроссплатформенную часть другие компании, можно посмотреть в результатах опроса jetbrains.

Android-разработка с использованием КМP никак не меняется, за исключением библиотек работы с сетью и хранения данных.  Многомодульные проекты уже стали стандартом в Android-разработке. А бизнес-логика пишется на чистом Kotlin без платформенных зависимостей в соотвествии с чистой архитектурой.   

Но для iOS-разработки при внедрении КМP есть нюансы, о которых мы поговорим дальше.

Kotlin

Не все iOS-разработчики реализовывают общую КМP-часть функциональности, но у нас в команде этим занимаются не только 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 года.

Окружение

В настоящий момент для работы с КМP и iOS мы используем 2 среды: Android Studio и Xcode.

Одной лишь Android Studio при разработке недостаточно: она не позволяет нормально работать с iOS-кодом. Подсветка синтаксиса, компиляция и запуск приложения работают (как это устроено под капотом, можно посмотреть в интервью с разработчиком плагина КМP), но навигация, подсказки, поиск использований — нет. В целом для iOS-разработчика пользоваться Android Studio приятно: удобная отладка, работа с терминалом и Git. Но она довольно требовательна к ресурсам.

Из-за ограничений студии разработчику нужно держать открытыми 2 среды — Android Studio и Xcode, а это повышает требования к машине разработчика. При этом много памяти съедает и система сборки Gradle. Но с 16Gb ОП вполне можно комфортно пользоваться сразу 2 IDE — Xcode и Android Studio на небольших проектах.

Скрины использования 2-х систем на разных машинах
16 Гб памяти (большое использование свопа)
16 Гб памяти (большое использование свопа)
32Гб памяти
32Гб памяти

Для решения мы пробовали использовать AppCode вместо двух IDE. В нём всё сразу из коробки, он понятный, если до этого имел дело с Android Studio. Но при этом он платный, и, к сожалению, недавно Jetbrains заявили, что он прекращает развитие. 

На сегодня мы видим оптимальным параллельное использование Xcode и Android Studio, если позволяют ресурсы машины.

Чтобы удостовериться, что на машине разработчика установлено всё необходимое ПО, используйте KDoctor.

Мы встречали проблемы со сборкой КМP-части на Mac с Apple silicon. Помогли решения из этой статьи. Изначально мы работали с Rosetta, что увеличивало время сборки, но с версии Kotlin 1.5.30 поддерживаются чипы Apple silicon.

Нюансы с использованием КМP

Связь common кода с iOS проектом

Работая с КМP в 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()
    }    
}

Кстати, этот пример можно реализовать с помощью KMP реализации Datastore от Google.

Ещё один пример, как реализовать взаимодействие с платформой в common-части: прокидывать closure в КМP-часть из нативной. Хотя это выглядит как костыль (приходится использовать глобальные переменные и методы) и иногда этого можно избежать. 

Пример с 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.KmpColors.core)
    }
}

До начала разработки на KMP в KTS Android-команда наработала библиотеки, которые нам удалось разделить на нативную и Kotlin-часть, а затем переиспользовать Kotlin-часть в КМP.

Реализация конкретных областей проекта

В этой части статьи мы рассмотрим основные аспекты любого проекта, как можно подойти к их решению с помощью KMP и какие библиотеки существуют.

От разработчика Jetbrains в открытом доступе есть список совместимых с КМP библиотек: https://github.com/terrakok/kmp-awesome. Внутри большое количество библиотек, присутствует разделение по разделам, так что ориентироваться там просто.

DI

Мы используем Koin в качестве DI в KMP части. Это самая популярная библиотека, поддерживающая 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)
        }
    }
}

Таким образом, методы из KMP FeatureDi вызываются только внутри Assembly.

Если требуется зависимость в KMP-части, привязанная к скоупу (аналог Swinject custom scope), то скоуп создаётся в koin. 

При необходимости можно сказать из нативной части, в какой момент закрыть скоуп:

class FeatureDi : KoinComponent {
   fun closeFeatureFlow() = getScope<FeatureScope>().close()
}

Такая прослойка позволяет сделать 2 DI фреймворка независимыми друг от друга.

Конечно, в идеальном мире должен быть 1 фреймворк для DI без каких-либо прослоек. Который будет поддерживать как нативные зависимости, так и кроссплатформенные. Так работает на Android с Koin. Но для iOS я таких реализаций пока не видел. Если вы знаете о таких — напишите в комментариях 👇  

Навигация

По этой части на текущий момент для KMP нет никаких готовых решений, которые будут поддерживать навигацию в 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. Он без проблем работает в KMP.

При работе с сетью у нас возникала проблема отправки файлов через 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 KMP не целился в эту сторону активно. Есть compose multiplatform, который стабильно работает на android и desktop, но на iOS только в alpha. Пример работы с ним можно посмотреть тут

Пока до стабильности compose multiplatform iOS далеко, поэтому мы используем нативный UI: SwiftUI, UIKit.

Но всё-таки в KMP можно шарить кое-что в презентационном слое (мы для этого используем MOKO-Resources):

Под капотом библиотека при компиляции проекта генерирует из КМP нативные ресурсы для каждой из платформ. 

Кроме этого, между платформами мы переиспользовали темы приложения таким образом, что при смене ночного режима тема автоматически меняется. Про эту реализацию мы планируем написать статью.

Отладка приложения

При отладке приложения всплывает проблема нескольких IDE. Можно либо дебажить с помощью Xcode c использованием плагина xcode-kotlin, либо с использованием AndroidStudio, AppCode. Наши разработчики используют оба подхода в зависимости от личных предпочтений. 

Краши

При возникновении краша в KMP-части всегда ошибка указывает на assembly code. Но для разбора причин ошибки помогает стектрейс потока. По нему можно приблизительно узнать, где произошла ошибка. Так же работает переход на KMP-файл при нажатии на фрейм стектрейса. Но точное место ошибки не подсвечивается.

В крашлитике также возможны проблемы с отображением логов. 

Для получения более подробной информации можно использовать CrashKit.

Логирование некритичных ошибок

Логирование некритичных ошибок КМP почти неизменно относительно нативной разработки.

  1. Создаём интерфейс ErrorReporter, который сможет логировать ошибки в сервис:

interface ErrorReporter {
    fun setUserId(userId: String)
    fun logError(throwable: Throwable)
}
  1. Создаём композитный репортер, чтобы уметь отправлять ошибки в разные сервисы:

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) }
    }
}
  1. Предоставляем платформенные логгеры:

//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. 

  1. В common cоздаем связь между логером и репортером. Мы в качестве логера используем Napier

class ErrorReporterAntilog(
    // в качестве ErrorReporter подставляется CompositeErrorReporter
    private val errorReporter: ErrorReporter 
) : Antilog() {
    override fun performLog(...) {
         errorReporter.logError(exception)
    }
}
//при запуске приложения в iOS native
Napier.releaseBuild(antilog: ErrorReporterAntilog…)
  1. После этого логируем ошибку в КМP, и она отправляется на обеих платформах, например в крашлитику. 

Napier.e("Error", throwable)

Утечки памяти

Один из пунктов, с которыми нужно иметь дело при разработке приложения — убедиться, что утечек памяти нет. В iOS с этим непросто и без KMP: отсутствуют инструменты, позволяющие своевременно заметить утечку памяти. 

Необходимо использовать Memory Graph Debugger, чтобы периодически проверять, что ничего не утекло. Либо писать тесты и запускать их на CI. На Android есть библиотека leakcanary, которая позволяет проверять базовые утечки, связанные с фреймворком. К сожалению, похожего решения на iOS нет.

С использованием КМP в iOS мы можем прибегать к тем же техникам отслеживания утечек, что и в нативе: 

  1. В какой-то момент понимаем, что что-то идёт не так 🙈:

  1. Открываем Memory Graph Debugger:

  1. Смотрим дополнительную информацию об утекающем объекте:

Как вы можете заметить из картинки выше — ничто в ней не указывает, где случилась утечка. И по нашему опыту в утечках всегда присутствуют кишки библиотек. Поэтому поиск проблемы может занимать длительное время.

Процесс

Для описания процесса рассмотрим проект с 4 мобильными разработчиками: 2 android / 2 iOS. iOS-разработчики не имели опыта в КМP до этого.

Сначала на проекте Android-команда разрабатывала KMP-фичи, а iOS-команда подключала их к себе уже готовые, накручивая только UI-часть.

Со временем iOS-разработчики подключились к код-ревью KMP-части в режиме наблюдения. Спустя несколько месяцев iOS-разработчики уже реализовывали внутри КМP шаблонные фичи и правки логики. Спустя еще пару месяцев iOS-разработка дошла до полноценной разработки в КМP.

Диаграмма
Диаграмма

В ноябре у части Android-команды был отпуск, поэтому iOS-разработчики делали бОльшую часть KMP задач. 

При рассмотрении графика необходимо понимать, что у нас в команде не стояло цели как можно быстрее научить iOS-разработку реализации КМP. С учетом существующего проекта с принятыми и устоявшимися подходами это можно сделать и быстрее.

Также мы прошли путь изменения разделения реализации фичи на независимые части: 

  1. Изначально на фичу ставилось 3 задачи: КМP, Android, iOS. Изначально выполнялась задача КМP, потом 2 задачи платформы. Этот подход плохо работал для нас: в момент реализации нативной задачи (например Android) оказывалось, что в KMP мы что-то упустили и требуется доработать KMP-модуль, а переделки влияли и на другую платформу, которая к тому моменту могла бы быть в процессе, и её приходилось перерабатывать. 

    Статистику количества таких задач мы не собирали, но по ощущению, их было около 60-70%. И каждый разработчик подмечал эту проблему на ретро. 

  1. Сейчас реализация задачи делится на 2 части: КМP + первая платформа, вторая платформа. То есть в начале разрабатывается KMP в паре с платформенной частью, например Android, если Android-команда первой подошла к реализации функциональности. После этого разрабатывается вторая платформенная часть, iOS. Так переделки в KMP удалось сократить. Хотя они всё-таки случаются, когда одной из платформ API оказывается неподходящим. 

Еще хотелось обратить внимание на один нюанс: т.к. мы используем монорепозиторий, любой разработчик может в своей задаче затронуть КМP-часть. А изменения в ней могут поломать платформы.

Пример. Android-разработчик поменял КМP, из-за этого сломалась iOS-часть, а разработчик этого мог даже не увидеть, потому что никогда не собирает iOS-проект у себя локально. Это можно контролировать тестами, но минимально достаточно на CI перед закрытием merge request добавить шаг «автосборка IOS и Android проектов». 

Чтобы ребята, реализующие КМP, понимали, как он интегрируется с платформами, в которых они не разбираются, мы провели сессии с объяснением базовых компонентов и их связью в Android и iOS. Еще эти сессии помогли разработчикам соседних платформ лучше локализовать ошибки и даже исправлять не очень сложные ошибки при изменении в КМP.

Теоретически, не у всех Android-разработчиков может быть рабочая машина с MacOS. В этом случае для интеграции КМP с iOS и правками понадобится помощь iOS-разработчиков.

Итог

Статус технологии

  • В КМP есть свои нюансы, которые мы обсудили в статье. Но за время разработки мы не встретили блокеров, которые заставили задуматься об отказе от КМP и вернуться в натив/перейти на другую кроссплатформенную технологию.

Личное впечатление

  • iOS-разработчики приняли КМP хорошо: у нас в команде у всех есть желание писать кроссплатформенную часть. Но при этом я уверен, что это не будет так для всех разработчиков в общем случае.

  • Команды iOS и Android стали более сплоченными: теперь обсуждаются не только общие концепции разработки, но и общая реализация.

Бизнес

В целом статья не направлена на обоснование КМP бизнесу, но без этих выводов выглядит неполной. 

  • Экономия времени разработки: зависит от проекта. 

    В наших текущих проектах идеальное сокращение времени составляет примерно 25%. Соотношение времени реализации бизнес-логики к UI 50-50. Значит, мы сокращаем реализацию бизнес-логики на одной из платформ. Реальное меньше: разработчикам нужно погрузиться в новую технологию, изредка случаются доработки KMP-части в зависимости от платформ и инфраструктурные сложности. 

    На сколько меньше точно, оценить сложно: для этого нужно сравнивать натив vs КМP на одинаковых проектах. Экономия будет больше, если в проектах много бизнес-логики находится на мобильном клиенте: офлайн-first решения, примеры таких мы рассматривали на vc (ПИК, Ascott). 

  • Экономия времени переделок при изменении только бэкенда/логики без изменения UI — почти 50%.

  • Время погружения разработчиков iOS в КМP с 0 до реализации комплексных фичей — около 4 месяцев. При том, что у нас в команде не стояло цели как можно быстрее научить iOS-разработку реализации КМP. С учетом существующего проекта с принятыми и устоявшимися подходами, это можно сделать быстрее

  • Bus factor увеличивается — доработать КМP-логику могут не только iOS-разработчики, но и Android, если у iOS не хватает времени, они заняты на другом проекте или ушли в отпуск

  • Нам удалось перейти на кроссплатформу без дополнительного найма людей и переучивания

  • Наш подход разработки в КМP-части не сильно отличается от нативного Android, который мы обкатывали давно. Что достаточно ускоряет погружение в КМP нового человека в команде: недавно к нам вышел новый Android-разработчик, и менее чем через неделю уже мог дорабатывать КМP часть. Часть из этого времени было потрачено на погружение в MVI-подход

Поделитесь в комментариях своим опытом внедрения КМP в команду мобильной технологии. Как iOS-разработчики задействованы в кроссплатформенной разработке?


Другие наши статьи по iOS-разработке:

Only registered users can participate in poll. Log in, please.
Используете ли KMP у себя в компании?
14% Я iOS-разработчик, используем, я разрабатываю KMP7
6% Я iOS-разработчик, используем, я НЕ разрабатываю KMP3
24% Я Android-разработчик, используем12
18% Не используем, планируем9
38% Не используем, не планируем19
50 users voted. 9 users abstained.
Tags:
Hubs:
Total votes 20: ↑19 and ↓1+18
Comments10

Articles

Information

Website
kts.tech
Registered
Founded
Employees
101–200 employees
Location
Россия