Kotlin Multiplatform
Общий код на нескольких платформах — объявления expect/actual, common- и платформенные source set, формат klib, интероп с iOS через Objective-C-заголовок и альфа-версию Swift export, модель памяти Kotlin/Native, Kotlin/Wasm, экосистема KMP-библиотек и стратегия внедрения.
18 вопросов
JuniorТеорияОчень частоЧто делают объявления expect и actual в модуле Kotlin Multiplatform?
Что делают объявления expect и actual в модуле Kotlin Multiplatform?
expect объявляет API в commonMain без тела, а каждый платформенный source set поставляет соответствующий actual. Цель, у которой actual отсутствует, не компилируется, поэтому платформенный шов проверяет компилятор, а не рантайм.
Типичные ошибки
- ✗Думать, что отсутствующий
actual— это падение в рантайме, а не ошибка компиляции - ✗Считать, что
expectнуждается в теле по умолчанию вcommonMain - ✗Принимать
expect/actualза наследование, а не за сопоставление объявлений по целям
Уточняющие вопросы
- →Может ли
actualрасширить видимость или изменить аргументы по умолчанию своегоexpect? - →Когда стоит внедрить интерфейс вместо ещё одного объявления
expect?
JuniorТеорияОчень частоЧем различаются commonMain и платформенные source set в модуле Kotlin Multiplatform?
Чем различаются commonMain и платформенные source set в модуле Kotlin Multiplatform?
commonMain компилируется под каждую цель, поэтому видит только общую стандартную библиотеку и никакой платформенный SDK. androidMain и iosMain компилируются под одну цель и вправе звать всё её API. Общий код дотягивается до них через expect/actual.
Типичные ошибки
- ✗Ждать, что JVM-API вроде
java.io.Fileвиден изcommonMain - ✗Считать source set папками, которые Gradle сливает, а не отдельными компиляциями
- ✗Забывать, что
commonMainобязан собираться под каждую объявленную цель, а не только под удобную
Уточняющие вопросы
- →Зачем нужен промежуточный source set вроде
iosMain, общий для двух целей iOS? - →Как добавление новой цели меняет то, что вправе вызывать
commonMain?
JuniorТеорияЧастоПочему мультиплатформенный HTTP-клиент Ktor можно вызывать прямо из commonMain?
Почему мультиплатформенный HTTP-клиент Ktor можно вызывать прямо из commonMain?
Клиент Ktor публикует один общий API плюс движок под каждую цель — OkHttp на Android, Darwin на iOS, — который подключает платформенный source set. API — мультиплатформенная библиотека, поэтому commonMain компилируется против него, а движок линкует каждая цель.
Типичные ошибки
- ✗Думать, что
Ktorпереписывает сетевой стек, а не делегирует его платформенному движку - ✗Считать, что цели iOS нужен рукописный
expect-мост для HTTP - ✗Забывать, что зависимость движка живёт в платформенном source set, а не в
commonMain
Уточняющие вопросы
- →Что сломается, если забыть объявить зависимость движка для одной из целей?
- →Какие ещё библиотеки вы ждёте увидеть в той же форме — общий API плюс движок?
JuniorТеорияЧастоЧто именно Kotlin Multiplatform разделяет между Android и iOS?
Что именно Kotlin Multiplatform разделяет между Android и iOS?
KMP разделяет Kotlin-логику — сеть, хранилище, валидацию, — компилируя её в JVM-библиотеку для Android и в нативный framework для iOS. UI по умолчанию остаётся платформенным; его шаринг — отдельный осознанный шаг через Compose Multiplatform.
Типичные ошибки
- ✗Считать, что KMP по умолчанию разделяет UI, хотя общей становится только логика
- ✗Думать, что общий модуль работает в интерпретаторе, а не компилируется в нативный framework
- ✗Полагать, что iOS получает транспилированный Swift, а не framework Kotlin/Native
Уточняющие вопросы
- →Какой артефакт компилятор Kotlin/Native отдаёт сборке Xcode?
- →Что добавляет поверх общей логики переход на Compose Multiplatform?
MiddleТеорияЧастоПочему kotlinx.serialization кодирует data class в commonMain без рефлексии?
Почему kotlinx.serialization кодирует data class в commonMain без рефлексии?
Это плагин компилятора, а не библиотека рефлексии. Он генерирует KSerializer для каждого класса с @Serializable на компиляции прямо из объявленной структуры, поэтому сериализатор — обычный код в выводе каждой цели и JVM-рефлексия ему не нужна.
Типичные ошибки
- ✗Считать, что
kotlin-reflectдоступен в общем коде на всех целях - ✗Думать, что аннотацию
@Serializableчитают в рантайме, а не плагином компилятора - ✗Ожидать, что
actual-сериализатор придётся писать руками под каждую платформу
Уточняющие вопросы
- →Что делает плагин, если класс с
@Serializableсодержит тип, для которого он не может сгенерировать сериализатор? - →Почему формат — JSON, protobuf, CBOR — остаётся выбором рантайма, а не компиляции?
MiddleТеорияИногдаПочему вызов freeze() больше не компилируется в современном модуле Kotlin/Native?
Почему вызов freeze() больше не компилируется в современном модуле Kotlin/Native?
freeze() всё ещё объявлен, но помечен устаревшим на уровне ошибки, поэтому вызов не компилируется. Он принадлежал старому менеджеру памяти, который убрали: современный Kotlin/Native использует трассирующий сборщик мусора и делит объекты между потоками незамороженными.
Типичные ошибки
- ✗Говорить, что
freeze()удалён, хотя он объявлен и лишь помечен устаревшим на уровне ошибки - ✗Считать, что Kotlin/Native до сих пор требует замораживать межпоточный объект
- ✗Полагать, что современный рантайм запрещает делить изменяемое состояние между потоками
Уточняющие вопросы
- →Если больше ничего не замораживается, что теперь защищает общее изменяемое состояние в Kotlin/Native?
- →В чём практическая разница между устареванием на уровне предупреждения и на уровне ошибки?
MiddleТеорияИногдаЧто такое klib и почему мультиплатформенная библиотека не может просто выпустить JVM-jar?
Что такое klib и почему мультиплатформенная библиотека не может просто выпустить JVM-jar?
klib хранит сериализованные объявления Kotlin плюс промежуточное представление, а не JVM-байткод. Jar везёт только байткод, который Kotlin/Native и Kotlin/JS не потребляют, поэтому зависимость общего кода приезжает как klib, опускаемый под каждую цель.
Типичные ошибки
- ✗Считать JVM-байткод переносимым форматом, который потребляет любая цель Kotlin
- ✗Думать, что
klib— это мешок готовых бинарников, а не объявления плюс IR - ✗Полагать, что Gradle-плагин умеет превращать jar в
klibпо требованию
Уточняющие вопросы
- →Что компилятор всё ещё обязан сделать с
klib, прежде чем появится бинарник iOS? - →Почему
klibпривязан к версии компилятора, которая его произвела?
MiddleДебаггингИногдаПочините общий Kotlin API, который команде iOS неудобно вызывать из Swift
Почините общий Kotlin API, который команде iOS неудобно вызывать из Swift
Kotlin/Native экспортирует через Objective-C-заголовок, а в Objective-C нет ни аргументов по умолчанию, ни исчерпывающих сумм: Swift требует все параметры и всё равно просит default. Добавьте явные перегрузки и дискриминатор, по которому switch исчерпывающий.
Типичные ошибки
- ✗Ждать, что аргументы по умолчанию Kotlin доживут до Objective-C-заголовка
- ✗Считать, что
sealed classостаётся исчерпывающим дляswitchв Swift - ✗Хвататься за
@JvmOverloads— это JVM-аннотация, на Kotlin/Native она ничего не делает
Уточняющие вопросы
- →Что происходит с
suspend-функцией, когда она переходит в Objective-C-заголовок? - →Как сохранить удобный Kotlin API для Android и всё же экспортировать удобный для Swift?
SeniorДизайнИногдаВам достаются два зрелых приложения — Android на 250 тысяч строк и iOS на 180 тысяч — реализующих один и тот же продукт. Руководство хочет Kotlin Multiplatform, а архитектор предлагает шестимесячную заморозку функций, за которую оба приложения перепишут на один общий модуль. Вы считаете постепенный путь безопаснее и хотите обосновать это конкретным планом. Опишите, как вы внедрите Kotlin Multiplatform без заморозки поставки: что первым уедет в общий модуль и почему именно этот срез, как каждое приложение будет потреблять общий модуль, как обе команды продолжат релизиться во время миграции и какие данные скажут вам остановить расширение общего модуля, а не продолжать.
Вам достаются два зрелых приложения — Android на 250 тысяч строк и iOS на 180 тысяч — реализующих один и тот же продукт. Руководство хочет Kotlin Multiplatform, а архитектор предлагает шестимесячную заморозку функций, за которую оба приложения перепишут на один общий модуль. Вы считаете постепенный путь безопаснее и хотите обосновать это конкретным планом. Опишите, как вы внедрите Kotlin Multiplatform без заморозки поставки: что первым уедет в общий модуль и почему именно этот срез, как каждое приложение будет потреблять общий модуль, как обе команды продолжат релизиться во время миграции и какие данные скажут вам остановить расширение общего модуля, а не продолжать.
Никакой заморозки: общий модуль — просто ещё одна зависимость, поэтому оба приложения продолжают релизиться. Первым уведите листовой срез без платформенных швов — модели плюс чистый алгоритм вроде цены. Расширяйте, пока он удаляет дубли; остановитесь, когда швы дороже.
Типичные ошибки
- ✗Соглашаться на заморозку поставки, хотя общий модуль потребляется как обычная зависимость
- ✗Начинать с платформенных краёв вместо листового среза с минимумом зависимостей
- ✗Считать целью полное покрытие кодовой базы, а не реально удалённые дубли
Уточняющие вопросы
- →Как опубликовать общий модуль, чтобы сборке iOS не понадобился тулчейн Kotlin?
- →Что делать, если первый общий срез заметно замедлит сборку iOS?
SeniorДебаггингИногдаОбщее состояние, меняемое из двух корутин, портится на iOS, но не на Android
Общее состояние, меняемое из двух корутин, портится на iOS, но не на Android
Современный менеджер памяти Kotlin/Native позволяет потокам делить изменяемые объекты, поэтому никто и не жалуется: это обычная гонка данных, ровно как на JVM. Заморозки больше нет, и она не решение — защитите состояние из общего кода диспетчером или Mutex.
Типичные ошибки
- ✗Хвататься за
freeze()— он запрещён на уровне ошибки и никогда не был примитивом синхронизации - ✗Считать, что Kotlin/Native до сих пор не даёт двум потокам менять один объект
- ✗Чинить это на стороне iOS, хотя незащищённое состояние лежит в
commonMainи Android тоже гоняет
Уточняющие вопросы
- →Почему тот же код на Android выглядит работающим куда чаще, чем на iOS?
- →Когда сведение к одному диспетчеру предпочтительнее взятия
Mutex?
SeniorТеорияИногдаЧем Swift export отличается от пути через Objective-C-заголовок и что везут в прод сегодня?
Чем Swift export отличается от пути через Objective-C-заголовок и что везут в прод сегодня?
Kotlin/Native везёт iOS через Objective-C-framework, который потребляет Swift, теряя аргументы по умолчанию, исчерпывающие суммы и точность обобщений. Swift export выпускает биндинги Swift напрямую и сохраняет больше, но он в статусе Alpha и в прод не годится.
Типичные ошибки
- ✗Считать, что Swift export уже стал умолчанием или стабильным путём
- ✗Полагать, что Objective-C-заголовок сохраняет аргументы по умолчанию и исчерпывающность sealed
- ✗Считать выбор косметическим, хотя он меняет, сколько модели Kotlin доживает до Swift
Уточняющие вопросы
- →Какие конструкции Kotlin — самые дорогие потери Objective-C-заголовка?
- →Что должно стать правдой про Swift export, прежде чем вы возьмёте его в релизную ветку?
SeniorТеорияИногдаЧем Kotlin Multiplatform отличается от кроссплатформенных фреймворков Flutter и React Native?
Чем Kotlin Multiplatform отличается от кроссплатформенных фреймворков Flutter и React Native?
KMP разделяет логику и компилирует её в нативный бинарник под каждую цель — без лишнего рантайма, VM или моста, — поэтому платформа сохраняет свой UI и доступ к API. Flutter везёт свой движок и виджеты; React Native управляет нативными вью из JavaScript.
Типичные ошибки
- ✗Думать, что KMP управляет нативными вью через мост в рантайме, как React Native
- ✗Считать, что KMP отдаёт общие экраны так же, как Flutter и React Native
- ✗Полагать, что виджеты Flutter — это родные контролы платформы, а не его собственного движка
Уточняющие вопросы
- →Что меняет в этом сравнении переход на Compose Multiplatform?
- →Когда общий UI на Flutter — правильный размен для продукта?
MiddleТеорияРедкоКакова текущая зрелость Kotlin/Wasm и где к нему стоит тянуться сегодня?
Какова текущая зрелость Kotlin/Wasm и где к нему стоит тянуться сегодня?
Kotlin/Wasm компилирует Kotlin в WebAssembly и пока в статусе Beta, а не Stable: пользоваться можно, но поверхность может сдвигаться между релизами, поэтому закладывайте перепроверку на обновлениях. Цели — браузер, где Compose Multiplatform рисует UI, и рантаймы WASI.
Типичные ошибки
- ✗Называть Kotlin/Wasm стабильным и считать его прямой заменой Kotlin/JS
- ✗Полагать, что WebAssembly вообще не способен вести браузерный UI
- ✗Планировать долгоживущий продукт на Beta-цели, не закладывая издержки обновлений
Уточняющие вопросы
- →К чему практически обязывает статус Beta на горизонте года релизов?
- →Как добавление цели Wasm меняет то, от чего вправе зависеть
commonMain?
SeniorДизайнРедкоКоманда год уводила код в commonMain, и это работало: доменные модели, правила валидации, расчёт цены, HTTP-слой и кеш на устройстве теперь живут там, и каждый переезд удалял дублирующую реализацию. Воодушевившись, команда предлагает увести туда ещё две вещи. Первая — код, который запрашивает у пользователя разрешение на геолокацию. Вторая — задача, которая синхронизирует заказы в фоне, пока приложение не на переднем плане. Senior-инженер возражает, что именно здесь схема перестаёт окупаться, но не может сформулировать правило, и команда принимает возражение за консерватизм. Дайте им критерий. Скажите, что относится к commonMain, а что нет, объясните, чем эти два предложения качественно отличаются от уже удавшихся переездов, и опишите шов для частей, обязанных остаться платформенными, — включая случай, когда шов не нужен вовсе и код просто остаётся нативным в каждом приложении.
Команда год уводила код в commonMain, и это работало: доменные модели, правила валидации, расчёт цены, HTTP-слой и кеш на устройстве теперь живут там, и каждый переезд удалял дублирующую реализацию. Воодушевившись, команда предлагает увести туда ещё две вещи. Первая — код, который запрашивает у пользователя разрешение на геолокацию. Вторая — задача, которая синхронизирует заказы в фоне, пока приложение не на переднем плане. Senior-инженер возражает, что именно здесь схема перестаёт окупаться, но не может сформулировать правило, и команда принимает возражение за консерватизм. Дайте им критерий. Скажите, что относится к commonMain, а что нет, объясните, чем эти два предложения качественно отличаются от уже удавшихся переездов, и опишите шов для частей, обязанных остаться платформенными, — включая случай, когда шов не нужен вовсе и код просто остаётся нативным в каждом приложении.
В commonMain уходит платформенно-независимое: модели, валидация, бизнес-правила, сеть и хранилище через KMP-библиотеки. Разрешения, фоновая работа и UI остаются нативными — за швом expect/actual лишь тогда, когда общий код обязан их вызывать.
Типичные ошибки
- ✗Считать шов
expect/actualбесплатным и тащить в общий код любой платформенный API - ✗Проверять по языку — пишется ли это на Kotlin — вместо платформенной независимости
- ✗Считать, что сеть и хранилище обязаны остаться нативными, раз упираются в платформенные API
Уточняющие вопросы
- →Где по этому правилу окажется view model, нужная обоим приложениям?
- →Какие данные заставят вас вынуть что-то обратно из
commonMain?
SeniorДизайнРедкоВаш общий модуль на Kotlin год как в проде, и команда iOS недовольна. Каждый вызов требует выписывать все аргументы, закрытая иерархия результата всё равно вынуждает ветку default, а сгенерированные имена читаются как Objective-C. Инженер предлагает включить Swift export, который выпустит биндинги Swift напрямую и сохранит большую часть модели Kotlin. Ближайший релизный поезд уходит через три недели и везёт правку платежей. Решите, включать ли Swift export сейчас, и защитите решение: что текущая зрелость означает для релизной ветки, что вы сделаете вместо этого для этого поезда и при каких условиях вернётесь к вопросу.
Ваш общий модуль на Kotlin год как в проде, и команда iOS недовольна. Каждый вызов требует выписывать все аргументы, закрытая иерархия результата всё равно вынуждает ветку default, а сгенерированные имена читаются как Objective-C. Инженер предлагает включить Swift export, который выпустит биндинги Swift напрямую и сохранит большую часть модели Kotlin. Ближайший релизный поезд уходит через три недели и везёт правку платежей. Решите, включать ли Swift export сейчас, и защитите решение: что текущая зрелость означает для релизной ветки, что вы сделаете вместо этого для этого поезда и при каких условиях вернётесь к вопросу.
Не тащите Alpha-возможность в релизную ветку, которая везёт платежи. Прототипируйте Swift export отдельно, а этот поезд везите на Objective-C-заголовке и заплатите за удобство там, где дешевле: тонким фасадом для Swift внутри общего модуля.
Типичные ошибки
- ✗Считать Alpha пометкой про оснастку, а не про сам генерируемый вывод
- ✗Полагать, что возможность можно запереть в один модуль и так удержать риск зрелости
- ✗Списывать трение интеропа на косметику, хотя оно бьёт команду iOS в каждом вызове
Уточняющие вопросы
- →Что должна показать ветка-прототип, прежде чем вы запланируете переход?
- →Как не дать фасаду для Swift протечь в вызовы со стороны Android?