Site icon AppTractor

Чему мы научились, используя KMM для iOS

Идея начать использовать 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 продолжит развиваться, он станет основным решением для кроссплатформенной разработки и может стать лучшей практикой для создания мобильных приложений.

Источник

Exit mobile version