Идея начать использовать Kotlin Multiplatform Mobile пришла к нам, когда мы проектировали реализацию GraphQL в Fiverr. Мы хотели сделать MVP, использующий GraphQL с Apollo, и увидели, что Apollo поддерживает KMM. Кстати, в то же время мы читали статью Netflix, которые используют KMM с большим успехом. Вроде бы все сошлось, поэтому мы взвесили наши варианты и решили попробовать.
И поддержка KMM в Apollo, и сам KMM являются экспериментальными или находятся в стадии бета-тестирования, поэтому мы знали, что столкнемся с вещами, которые не будут работать должным образом, и что потребуется время для обучения, особенно для iOS-разработчиков, которые были в ужасе от IntelliJ и Kotlin. Но это был вызов, к которому мы были готовы.
Это должно было стать первым MVP, впервые использующим KMM и GraphQL, было очевидным выбором написать его один раз для обеих платформ и использовать возможности мультиплатформенности. В этой статье мы расскажем о том, что узнали как iOS-инженеры, которым пришлось погрузиться в KMM и бороться с неизвестными тварями, такими как Kotlin и Gradle.
Как связаны между собой наш MVP и KMM
Я думаю, прежде чем мы углубимся в нашу реализацию, важно сначала понять, что было нашим MVP и как мы хотели использовать KMM.
Наш MVP состоит из модуля KMM, который выполняет запросы GraphQL. Модуль KMM предоставит интерфейс для использования приложением и блок ответа. Вся сетевая логика, обработка GraphQL, модели и Apollo будут находиться внутри модуля KMM, и приложение не будет обращать внимания на то, как все делается. Таким образом, мы также можем изменить реализацию модуля KMM с Apollo на другую библиотеку, не заботясь о самом приложении.
Одним из преимуществ Apollo является его способность автоматически генерировать классы на основе вашей схемы GraphQL. Так, например, если бы в вашей схеме был объект для Order, то Apollo автоматически сгенерировал бы класс Kotlin под названием Order со всеми его соответствующими полями.
Проблема заключалась в том, что мы не могли напрямую использовать сгенерированные Kotlin классы ни в Android, ни в iOS, потому что у них были странные имена и запутанная внутренняя структура, которые выглядели бы очень уродливо в клиентском коде. Поэтому мы решили (1) создать Data Transfer Object (DTO) в нашем проекте KMM, которые были бы отражением моделей, которые у нас уже были в приложении (например, у нас уже была модель под названием Order для запроса REST) и (2) преобразовать автоматически сгенерированный Классы Kotlin для DTO.
В конце концов, клиенты будут работать с DTO, которые будут более читабельными и удобными в работе.
IDE, настройка окружения и Kotlin
Чтобы начать работать с KMM, вам нужно установить Android Studio или IntelliJ для кода на Kotlin и собрать проект KMM. Так что нашим iOS-инженерам пришлось отказаться от безопасной гавани Xcode и научиться работать с новой IDE и инструментами. Здесь есть небольшая кривая обучения для iOS-инженеров, и установка новой IDE и среды не была для нас такой простой, как установка Xcode.
Помимо очевидных различий между Xcode и IntelliJ (мы все знаем, как щепетильны разработчики в отношении сочетаний клавиш), есть еще и обучение обращению с Gradle и управлению пакетами. Нам потребовалось несколько дней, чтобы все настроить (с помощью наших замечательных Android-инженеров) и начать писать код на Kotlin.
Kotlin и Swift оба являются высокоуровневыми языками, и, как правило, если кто-то использует Swift, ему будет легче понять Kotlin, но важно отметить, что между этими двумя языками больше различий, чем сходств. как в синтаксисе, так и в том, как вещи реализованы под капотом (например, параллелизм). Здесь кривая обучения более крутая, и нашим iOS-разработчикам очень помогли наши партнеры из Android-команды. Вы можете узнать больше о Kotlin и Swift здесь.
Хотя мы стали лучше использовать Kotlin, мы все еще далеки от профессионализма, так как чистое время, которое мы потратили на работу над KMM-проектом, невелико по сравнению с работой над приложением.
Параллелизм
Одно из самых больших различий между Kotlin и Swift заключается в том, как обрабатывается параллелизм, и, прежде всего, параллелизма в Kotlin/Native, мягко говоря, немного не хватает. Текущая реализация имеет много недостатков и довольно сложна (в настоящее время JetBrains работает с новой моделью памяти, которая на момент написания этой статьи находится в альфа-версии).
Вдобавок ко всему, стабильные версии Kotlinx.coroutines не поддерживают параллелизм на платформах Apple, а поддерживается только основной поток. Существуют альтернативные библиотеки, которые можно использовать для добавления конкурентности а платформах Apple, и это то, что мы сделали. Это потребовало от нас написания дополнительного кода для каждой платформы (используя возможности KMM для доступа к API для конкретных платформ).
Вы можете увидеть нашу обертку ниже во всех ее кровавых деталях:
object MainLoopDispatcher : CoroutineDispatcher(), Delay { override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatch_get_main_queue()) { block.run() } } override fun scheduleResumeAfterDelay( timeMillis: Long, continuation: CancellableContinuation<Unit> ) { dispatch_after( dispatch_time(DISPATCH_TIME_NOW, timeInSeconds / seconds), dispatch_get_main_queue() ) { with(continuation) { resumeUndispatched(Unit) } } } override fun invokeOnTimeout( timeMillis: Long, block: Runnable, context: CoroutineContext ): DisposableHandle { val handle = object : DisposableHandle { var disposed = false private set override fun dispose() { disposed = true } } dispatch_after( dispatch_time(DISPATCH_TIME_NOW, timeInSeconds / seconds), dispatch_get_main_queue() ) { if (!handle.disposed) { block.run() } } return handle } }
Распространение
После того, как мы закончили разработку в KMM, пришло время создать фреймворк и добавить его в наше приложение для iOS. К сожалению, это тоже было не так просто, и нам пришлось проделать дополнительную работу.
В момент разработки можно создавать отдельные фреймворки только для x86 (симулятор) или ARM. Так что если вам нужен фреймворк, поддерживающий и то, и другое, нужно будет написать скрипт в Gradle, который объединит их и создаст XCFramework.
Это была непростая задача, так как документации практически нет, и нам пришлось изучить, как работает Gradle (он намного сложнее, чем SPM). Получив XCFramework, мы создали для него отдельный репозиторий и пакет Swift, который можно легко подключить к любому проекту с помощью SPM.
Отладка
iOS-инженеры работают в Xcode. Если они столкнутся с проблемой в библиотеке KMM, они хотели бы иметь возможность отладки, чтобы найти проблему. К сожалению, отладка библиотеки KMM была для нас просто невозможна. Хотя есть плагин Xcode, созданный другой компанией (подробнее здесь), который позволит вам отлаживать файлы Kotlin в Xcode, мы так и не смогли заставить его работать на Xcode 13, и мы были не единственными.
Нам пришлось полагаться на использование print-ов и на добрую волю команды Android, которая без проблем отлаживала библиотеку. Очевидно, что это неудобно и отрицательно сказывалось на времени разработки.
Поддержка Swift и совместимость с Objective-C
Как мы упоминали ранее, компилятор Kotlin/Native создает фреймворк iOS. Компилятор Kotlin генерирует биндинги Objective-C для базового кода Kotlin. На данный момент Kotlin/Native может генерировать только код Objective-C, и у JetBrains нет понимания, когда они добавят биндинги Swift. Подробнее о совместимости с Objective-C можно прочитать здесь.
Отсутствие поддержки Swift создает несколько проблем, но еще больше усугубляется различиями между Swift, Objective-C и Kotlin. Нам пришлось создать дополнительный слой, который служит мостом между объектами КММ и нашими. Дополнительный слой имел код, который имел дело с кодом Objective-C из KMM и преобразовывал его, чтобы наши объекты Swift могли быть правильно инициированы. Этот же уровень занимался отправкой данных в модуль KMM, что также требовало некоторых преобразований. Это добавляет определенные накладные расходы для каждого объекта KMM, с которым вы хотите работать.
Кроме того, некоторые вещи полностью ломаются, например, Enums или Generics. Перечисления Enums, написанные на Kotlin, переводятся в Objective-C как объекты со свойствами, соответствующими кейсам enum. Это создает действительно уродливый код.
//These are parameters we send back to the KMM module. //Kotlin primitive type boxes are mapped to special Swift/Objective- //C classes. KotlinInt is a representation of Int? in Kotlin.
var sortByAsKotlinInt: KotlinInt? = nil var filterByAsInt: Int32? = nil // Represents Int in Kotlin // Kotlin has interoperability only with Objective-C so you have to use NSMutableArray.
var usernamesAsNSMutableArray: NSMutableArray? = nil
// sortBy is defined as Int Enum in Swift. We have extended Int to
// have a method to turn it into KotlinInt.
sortByAsKotlinInt = sortBy.rawValue.kotlinInt()
// usernames is a Swift Array that we have to convert to Objective-C
// array.
usernamesAsNSMutableArray = NSMutableArray(array: usernames)
// That's how a String Enum was translated from Kotlin to Objective-
//C by KMM. Properties correspond to the enum cases.
@interface MPFGQLOrdersStatusFilter : MPFGQLKotlinEnum<MPFGQLOrdersStatusFilter *> <MPFGQLApollo_apiEnumValue> @property (class, readonly) MPFGQLOrdersStatusFilter *updates __attribute__((swift_name("updates"))); @property (class, readonly) MPFGQLOrdersStatusFilter *active __attribute__((swift_name("active"))); @property (class, readonly) MPFGQLOrdersStatusFilter *cancelled __attribute__((swift_name("cancelled"))); @property (class, readonly) MPFGQLOrdersStatusFilter *completed __attribute__((swift_name("completed"))); @end
Итого
В этой статье я рассказал о некоторых основных задачах и проблемах, с которыми мы столкнулись при работе с KMM на iOS. Я не рассказал обо всем, но этого вполне достаточно, чтобы дать вам представление о KMM. Несмотря на проблемы и более длительное, чем ожидалось, время разработки, MVP был успешным, и у нас был рабочий модуль KMM, который использовался на двух платформах сразу. Мы продолжали работать с КММ, и теперь он с большим успехом используется в продакшене.
KMM все еще может быть в ранней стадии разработки, но если вы проявите упорство, несмотря на все трудности, вы, безусловно, сможете использовать его. Моей целью при изложении проблем было не напугать вас, а поделиться с вами трудностями использования KMM в сегодняшнем состоянии. В любом случае, я думаю, что если KMM продолжит развиваться, он станет основным решением для кроссплатформенной разработки и может стать лучшей практикой для создания мобильных приложений.