Алексей Гладков

Mobile Developer в Тинькофф

В мае 2023 года команда ГК Юзтех организовала в Томске Usetech Meetup «Тренды мобильной разработки», где своим опытом поделились эксперты российского ИТ-рынка. По итогам мероприятия мы написали серию статей, каждая из которых посвящена актуальным вопросам и транслирует выступление одного из спикеров. Начнем с выступления Алексея Гладкова, Mobile Developer компании Тинькофф.

Про Kotlin Multiplatform (КММ) многие слышали, но пробовали далеко не все. Мы с командой использовали его в работе, и здесь я расскажу о своем опыте. Возможно, теперь у вас появится понимание, как аргументировать бизнесу зачем вообще нужен KMM и насколько это сейчас рабочая история. 

Для начала пару слов о себе: меня зовут Алексей Гладков, работаю в компании «Тинькофф», преподаю в МФТИ, пишу нативные приложения уже около 10 лет, веду ютуб-канал про мобильную разработку «Mobile Developer».

Доклад, с которым я выступал в рамках митапа, называется «The State of Kotlin Multiplatform», поскольку все время выходят какие-то новые фичи, и я его дополняю. Для меня это, условно, дайджест, который я регулярно обновляю. Сейчас я расскажу о текущем состоянии Kotlin примерно на начало апреля 2023 г. 

Почему вообще надо задумываться о мультиплатформенном подходе? В 2015 году (еще даже не вышли часы Apple Watch) мы, мобильные разработчики, в основном ориентировались на телефоны. Другие разработчики ориентировались на планшеты и ноутбуки. То есть было четкое разделение. К 2023 году ситуация изменилась. Теперь ко мне могут прийти и сказать: «Мы на телевизоре хотим запуститься» или «На часах». Вполне себе реальная история. Дальше эта тенденция будет только развиваться – у нас будут появляться:

  • новые операционные системы (Harmony, Aurora и др.); 

  • разные форм-факторы (стенды, сканеры и т.д.).

Например, к нам пришли из Леруа с запросом сделать стенд – кассу оплаты для самообслуживания. Если вдуматься, это просто огромный экран, похожий на телефон. На нём тоже, как ни странно, крутится Android, и там тоже можно делать приложения. 

Также одна из тенденций в том, что люди хотят работать омниканально и не хотят зависеть от обстоятельств. Взять, например, Telegram: мы привыкли, что можно там написать что-то, сесть в такси и в нем продолжить переписываться, потом на работе переписываться с ноутбука, а приехав домой продолжить с компьютера. Ты все время живешь в Telegram и твой User Experience не сильно отличается. При этом, где бы ты ни оказался, ты можешь открыть нужное приложение и продолжить работу.

Следовательно, наша задача состоит в том, чтобы перейти из Android или iOS к мультиплатформенной разработке.

У Apple тоже есть платформенная разработка, только она специфическая. С ней ты можешь делать продукты под планшеты и под компьютеры, но под планшеты и под компьютеры компании Apple. Соответственно, мультиплатформа вроде есть, но вроде её и нет.

Тут возникает вопрос: «А почему Kotlin? Есть же еще Flutter или React Native». Когда к нам пришел запрос от Леруа, мы начали думать, какую технологию взять.

Плюсы Flutter:

  • Работает на всех платформах (iOS, Android, Desktop, Web);

  • Большой набор готовых виджетов (Google проделал огромную работу, чтобы адаптировать все  виджеты);

  • Поддержка Google;

  • Большое комьюнити.

Есть и минусы:

  • Dart. Ему нужно обучать ВСЕХ сотрудников;

  • Отсутствие нативного look and feel;

  • Поддержка Google. Почему я этот пункт записал и в минусы? Потому что есть такое «кладбище проектов Google». Google славится не только тем, что разрабатывает какие-то новые вещи — он их также легко закапывает. Тот же Dart: изначально он был веб-технологией, потом Google ее закопал, а затем в какой-то момент снова достал и воскресил. Теперь, соответственно, она живет, но как долго — непонятно;

  • Отсутствие больших историй успеха. Я имею в виду приложения уровня мобильных банков, над которыми работают сотни разработчиков. Есть отдельные истории успеха у Яндекса, Google и т.д. В чем особенность гигантских приложений? Над ними работают сотни людей. Виджеты и всё остальное – это классно, но когда над продуктом работает огромная команда, начинаются другие сложности, поверьте.

Перейдем к React Native. Его плюсы:

  • Работает на всех платформах (iOS, Android, Desktop, Web);

  • Большой набор готовых компонентов;

  • Огромный набор готовых решений и библиотек;

  • Большая база готовых инженеров.

Минусы:

  • Плохая производительность;

  • Тяжелые обновления;

  • Трудная миграция существующего приложения.

Теперь о плюсах Kotlin Multiplatform:

  • Легкая интеграция в существующее приложение;

  • Большая база готовых инженеров;

  • Большое количество готовых решений;

  • Работает на Android, Desktop, iOS*, MacOS*. На iOS и MacOS есть некоторые нюансы заключаются в интерфейсе;

  • Нативный look and feel.

Минусы Kotlin Multiplatform:

  • Необходимость писать отдельный UI для платформы* (со звездочкой, потому что это не минус для крупных компаний, у которых есть свои дизайн-системы, библиотеки и много всего, что сделано под нативный UI);

  • фундаментальный Gradle. 

Давайте пару слов о том, как это устроено. У нас есть приложение: если брать чисто архитектуру, то это Data-логика, какая-то бизнес-логика и слой UI. Пошарить можно все, включая объем модели в некоторых ситуациях. Можно пошарить даже навигацию, нельзя пошарить только сам UI. 

Это важный момент: в большинстве случаев пошарить можно около 90% приложения. Соответственно, в КММ есть понятие SourceSets. Под каждый таргет создается свой SourceSet. Причем под разные архитектуры процессоров тоже создаются разные SourceSets. Их можно объединять специальными объединяющими соурсетами, которые позволяют упрощать определенные вещи. Например, сделать iOS Main, чтобы не прописывать какие-то вещи дважды или трижды. Но во время компиляции всё это откинется и останется только Command Main и ваш таргет. Все вместе скомпилируется и получится ваш артефакт.

Как это все происходит? У вас есть Kotlin-модуль. У компилятора есть несколько стадий: Frontend компилятора, Backend компилятора и там посередине соответственно. Посередине, как раз, так называемая интермедия reprezentative code, из которого Frontend Kotlin-компилятора берет то, что мы написали, разворачивает и получает промежуточный код, который может подсунуть разным другим компиляторам. На выходе у нас получаются вполне себе нативные .jar и .framework – нативные артефакты. 

Вернемся к приложениям. Любое приложение состоит из следующих компонентов:

Мы сейчас пройдемся по этим составляющим и посмотрим, что есть по библиотекам:

  1. KMM Awesome. Сделал его Константин Цховребов из компании JetBrains. Здесь можно найти все библиотеки по KMM. Главное условие – собраны именно те, которые работают и на Android, и на IOS. На Desktop, при этом, могут работать или нет. Все библиотеки разбиты по разделам, очень удобно.

  1. Для сети JetBrains сделали свою библиотеку Ktor Client. Работает на IOS, Android, Desktop и Web. Настраивается в декларативном котлиновском виде, поскольку это чистый Kotlin. Мы создаем Client, очень легко устанавливаем туда любые плагины:

val client = HttpClient(CIO) {

    install(Logging) {

        logger = Logger.DEFAULT

        level = LogLevel.HEADERS

    }

}

После мы можем дергать запросы и настраивать так, как вы хотите:

client.get {

    url {

        protocol = URLProtocol.HTTPS

        host = "ktor.io"

        path("docs/welcome.html")

    }

}

Все настраивается декларативно: в тех вещах, которые вы не прописываете, прописываются некоторые дефолтные значения. Очень гибко и удобно, мы во всех проектах Android перешли на Ktor.

  1. SQL Delight – база данных, так же работает на IOS, Android, Desktop и Web. Это Gradle-плагин, мы пишем:

sqldelight {

   databases {

       create("Database") {

          packageName.set("tech.mobiledeveloper") 

       }

   }

}

Всё, что нам нужно, готово. Дальше нам нужно прописать SQL-файлы:

// src/commonMain/sqldelight/data/daily.sq

CREATE TABLE HabitEntity (

 itemID INTEGER PRIMARY KEY AUTOINCREMENT,

 title TEXT NOT NULL,

 isGood INTEGER DEFAULT 0

);

Тут у некоторых Senior-инженеров может наступить ступор, потому что SQL-файлы надо писать самому и вспоминать :) Это единственный недостаток. Для этого нужно установить специальный плагин и можно будет опять ничего не писать – очень удобно. 

  1. Навигация. Кто писал библиотеку для навигации? Их очень много – я тоже написал свою, мне показалось, что она будет полезна. Есть Decompose – это в KMM-мире стандарт для очень больших, сложных проектов. Почему для больших и сложных? Потому что есть поддержка всех платформ, есть State Saving – это архитектурный компонент в первую очередь. Это не совсем навигация в прямом смысле, а скорее задел под полноценную архитектуру, включая навигацию. У него еще хорошая документация и он работает как с декларативом, так и без. 

Минус в том, что там создается очень много вспомогательных классов. Соответственно, если это маленький проект, то вы будете писать больше вспомогательных классов, нежели получать удовольствие от работы. И да, минус еще в том, что это не является навигацией в прямом смысле, если у вас есть архитектура, то этот компонент вам не подойдет.

  1. Voyager – это тоже KMM-библиотека, но она подходит для compose. Тоже есть State Saving, поддержка готовых UI компонентов, хорошая документация. Минус – требует создавать класс для каждой composable-функции.

  1. Odyssey – моя собственная библиотека. Она тоже для compose, работает на IOS, Android, Desktop, Web и MacOS. Единственная разница от Voyager в том, что здесь не надо создавать классы. Из минусов: плохая поддержка deeplink и нет поддержки смены ориентации.

  1. Теперь перейдем к ресурсам. Libres – она работает тоже на все платформы. Всё, что вам нужно, это настроить плагин:

// build.gradle.kts

libres {

   generatedClassName = "AppRes"

   generateNamedArguments = true

   baseLocaleLanguageCode = "en"

}

После этого вы работаете строго как в Android:

// src/commonMain/libres/strings/strings_en.xml

<resources>

   <string name="app_name">JetpackComposeDemo</string>

   <string name="days_today">Today</string>

   <string name="title_font_size">Font size</string>

   <string name="title_font_size_small">Small</string>

   <string name="title_font_size_medium">Medium</string>

   <string name="title_font_size_big">Big</string>

</resources>

// src/commonMain/kotlin/SomeScreen.kt

Text(

   modifier = Modifier.padding(top = 24.dp),

   text = AppRes.string.compose_success_add,

   style = JetHabitTheme.typography.body,

   color = JetHabitTheme.colors.primaryText

)

Для IOS единственное, что меняется,  – добавляется:

// iOS

AppRes.shared.string.compose_success_add

С картинками то же самое: ничего делать не нужно, закидывайте картинки и вперед:

// src/commonMain/libres/images/ic_calendar.svg

// src/commonMain/libres/images/ic_calendar.png

TabConfiguration(

   title = "Daily",

   selectedIcon = painterResource(AppRes.image.ic_calendar),

   unselectedIcon = painterResource(AppRes.image.ic_calendar),

)

В IOS, соответственно, так:

// iOS

AppRes.shared.image.ic_calendar
  1. DI. В последнее время я не сторонник DI-фреймворков, но Kodein – это удобный инструмент, чтобы не писать DI самому. Очень простой, легко настраивается и используется. 

Что по поводу всего остального? Мы проговорили про навигацию, UI, сеть, базу данных, ресурсы и DI, но у нас еще есть firebase, собственные системы, аналитика, криптография и так далее. Это тоже нужно как-то заворачивать в KMM и вот для этого готовых решений нет. Тут я хочу показать способ, который мы сделали. С помощью него можно завернуть в KMM практически любое нативное решение. К примеру, нам нужно сделать аналитику: аналитика на firebase, которого нет в библиотеках KMM. Мы делаем интерфейс в Common-коде, это чисто котлиновский код, у которого есть одна функция – trackEvent (мы могли сюда вставить любые другие функции, но нам нужна только trackEvent). 

// src/commonMain/kotlin/analytics/…

interface AnalyticsTracker {

   fun trackEvent(event: AnalyticsEvent)

}

interface AnalyticsEvent

Дальше мы AnalyticsEvent расписываем в специальный AnalyticsEventFB:

abstract class AnalyticsEventFB : AnalyticsEvent {

   abstract val name: String

   open var params: Map<String, Any> = emptyMap()

   override fun toString(): String {

       return "AnalyticsEventFB(name='

name', params=" class="formula inline">params)"

   }

}

После этого мы создаем наши ивенты, уже для конкретных экранов, перераспределяем параметры – это все еще чистый код:

sealed class WebViewScreenEvents(

   override val name: String,

   override var params: Map<String, Any> = emptyMap()

) : AnalyticsEventFB() {

   data class WebApplicationInitialized(...) : WebViewScreenEvents(

       name = "web_app_initialized",

       params = hashMapOf(...)

   )

}

По сути, мы уже реализовали все ивенты. Нам теперь нужен просто провайдер, чтобы добавить это в Firebase. Здесь, как раз, мы делаем специальный класс на платформе Firebase Tracker, в нем мы просто засовываем наш провайдер и определяем функцию трека:

class FirebaseTracker  constructor(

private val application: Application

) : AnalyticsTracker {

 private val firebaseAnalytics: FirebaseAnalytics by lazy { 

FirebaseAnalytics.getInstance(application.applicationContext)

 }

   fun track(event: AnalyticsEventFB) {

	// Send event to firebase here

   }

}

По итогу все работает. И все, что нам надо было для этого сделать – написать один единственный класс. 

Теперь давайте вернемся к минусам Kotlin Multiplatform, а именно, к необходимости писать отдельный UI для платформы. Здесь нам на помощь приходит Compose Multiplatform – это отдельный фреймворк. Кому интересно, можете зайти посмотреть.

Характеристики Compose Multiplatform:

  • Работает, используя Skiko (mpp bindings for Skia);

  • Полная копия Jetpack Compose;

  • Полный интероп с платформами.

ИТОГИ:

  • Большинство вещей уже работает из коробки (сеть, база данных, навигация, и т.д.);

  • Можно писать приложение полностью в commonMain;

  • Есть выбор при реализации UI слоя (можете использовать Compose, можете использовать Native, как вам удобно);

  • Compose активно допиливается (я очень надеюсь, что к концу этого года мы увидим бета версию на iOS или вообще релиз);

  • Kotlin – прекрасный, удобный язык. Я думаю, многие со мной согласятся.

На этом у меня все. Делитесь своими мнениями в комментариях!

Комментарии (3)


  1. ris58h
    14.07.2023 08:02

    The State of

    Доклад на английском что ли? Вроде нет.


  1. Pastoral
    14.07.2023 08:02
    +1

    Люди со стороны, простые и наивные, по крайней мере некоторые, понимают это так.

    KMM и Kotlin Native - две технологии в документации по Kotlin не связанные, было бы в https://kotlinlang.org/lp/multiplatform/ вместо Common Multiplatform написао Kotlin Native - было бы понятней, но нет. Внешне, по словам описания, что Native что Multipatform Mobile - одно и то же, может KMM включает надстройку над Native для Firebase и/или связи с нативными компонентами. При этом явно это не говорится и KMM официально всё ещё в бете. Иными словами - дело ясное что дело тёмное.

    Альтернатива - Flutter, фактически в альфе. Потому в альфе, что с существующим рендером Skia на iOS толком не работает и 3D не поддерживает и с Интернетом не очень, а без этого спрашивать истории успеха - садизм. Зато Гугол уже делает нормальный рендерер с поддержкой 3D, а все остальные продолжают сидеть на Skia, с единственной альтернативой перелезть на SDL и со всеми вытекающими последствиями, включая отсутствие необходимлсти их рассмотрения. А добрые люди в это время делают Web Assembly со сборщиком мусора.

    С минусами Flutter простому человеку тоже не всё ясно. С яркими успехами уже разобрались.

    Поддержка Гугол - конечно да, приговор, но есть шанс что на этот раз это действительно другое, как раз по причине Эппловской якобы ущербной кроссплатформенности. У Гугла выбор - либо развивать кроссплатформу и в итоге вырваться со смартфонов, либо медленно увядать. Разведпризнаки - Гугол выдал альфу и бету за релиз и остервенело ужесточает впаривание рекламы, а Эппл занялся геймдевом на платформе.

    Отсутствие нативного look and feel является преимуществом, есть много примеров провалов кроссплатформы (фактически всей, если не по доброму) к тому стремившейся. Был хороший анализ причин успеха Flutter, а он явно нуждается в объяснении, где это было основной причиной, ссылку/источник не помню. Сейчас Flutter, согласно официальным планам, ставит себе задачу стать абсолютно лучшим UI framework - как по мне, грубая но не обязательно фатальная ошибка.

    Dart учить не нужно, достаточно прочиать по диагонали. Если, конечно, человека вообще можно допускать до компьютера. Хотя да, от бесконечного изобретательства языков программирования все уже устали до истерики (привет Carbon). Зато Flutter собирает всю мобильную мерзость сам, что надо было бы записать в преимущества раз уж прелести Gradle записаны Kotlin в недостатки.


  1. quaer
    14.07.2023 08:02

    Как в KKM выполняется локализация в приложении для ПК?