Углублённые концепции структуры многоплатформенного проекта
В этой статье рассматриваются углублённые концепции структуры проекта Kotlin Multiplatform и их соответствие реализации в Gradle. Эта информация пригодится, если вам нужно работать с низкоуровневыми абстракциями сборки Gradle (конфигурациями, задачами, публикациями и другими) или создавать плагин Gradle для сборок Kotlin Multiplatform.
Эта страница может быть полезна, если вы:
Хотите совместно использовать код между набором целевых платформ, для которых Kotlin не создаёт набор исходного кода.
Хотите создать плагин Gradle для сборок Kotlin Multiplatform или вам нужно работать с низкоуровневыми абстракциями сборки Gradle, такими как конфигурации, задачи, публикации и другие.
Один из важнейших аспектов, которые нужно понимать при управлении зависимостями в многоплатформенном проекте, — это разница между зависимостями проектов или библиотек в стиле Gradle и отношением dependsOn между наборами исходного кода, характерным для Kotlin:
dependsOn— это отношение между общими и платформенными наборами исходного кода, которое обеспечивает иерархию наборов исходного кода и совместное использование кода в многоплатформенных проектах в целом. Для наборов исходного кода по умолчанию иерархия настраивается автоматически, но в некоторых случаях может потребоваться изменить её.Зависимости библиотек и проектов в целом работают как обычно, но для правильного управления ими в многоплатформенном проекте нужно понимать, как зависимости Gradle разрешаются в детализированные зависимости набор исходного кода → набор исходного кода, используемые при компиляции.
dependsOn и иерархии наборов исходного кода
Обычно вы будете работать с зависимостями, а не с отношением dependsOn. Однако изучение dependsOn крайне важно для понимания того, как устроены проекты Kotlin Multiplatform.
dependsOn — это специфичное для Kotlin отношение между двумя наборами исходного кода Kotlin. Например, это может быть связь между общим и платформенным наборами исходного кода, когда набор исходного кода jvmMain зависит от commonMain, iosArm64Main зависит от iosMain и так далее.
Рассмотрим общий пример с наборами исходного кода Kotlin A и B. Выражение A.dependsOn(B) сообщает Kotlin, что:
Aвидит API изB, включая внутренние объявления.Aможет предоставлять фактические реализации для ожидаемых объявлений изB. Это необходимое и достаточное условие, посколькуAможет предоставлятьactualsдляBтогда и только тогда, когдаA.dependsOn(B)прямо или косвенно.Bдолжен компилироваться для всех целевых платформ, для которых компилируетсяA, в дополнение к собственным целевым платформам.Aнаследует все обычные зависимостиB.
Отношение dependsOn создаёт древовидную структуру, известную как иерархия наборов исходного кода. Вот пример типичного проекта для мобильной разработки с android, iosArm64 (устройства iPhone) и iosSimulatorArm64 (симулятор iPhone для Mac с Apple Silicon):
Стрелки обозначают отношения dependsOn. Эти отношения сохраняются при компиляции платформенных бинарных файлов. Благодаря этому Kotlin понимает, что iosMain должен видеть API из commonMain, но не из iosArm64Main:
Отношения dependsOn настраиваются с помощью вызова KotlinSourceSet.dependsOn(KotlinSourceSet), например:
kotlin {
// Targets declaration
sourceSets {
// Example of configuring the dependsOn relation
iosArm64Main.dependsOn(commonMain)
}
}
В этом примере показано, как отношения
dependsOnможно определить в скрипте сборки. Однако плагин Kotlin Gradle по умолчанию создаёт наборы исходного кода и настраивает эти отношения, поэтому делать это вручную не нужно.Отношения
dependsOnобъявляются отдельно от блокаdependencies {}в скриптах сборки. Это связано с тем, чтоdependsOn— не обычная зависимость, а специфичное отношение между наборами исходного кода Kotlin, необходимое для совместного использования кода на разных целевых платформах.
Нельзя использовать dependsOn для объявления обычных зависимостей от опубликованной библиотеки или другого проекта Gradle. Например, нельзя настроить зависимость commonMain от commonMain библиотеки kotlinx-coroutines-core или вызвать commonTest.dependsOn(commonMain).
Объявление пользовательских наборов исходного кода
В некоторых случаях может понадобиться промежуточный набор исходного кода, созданный вручную. Рассмотрим проект, который компилируется для JVM, JS и Linux, и в котором нужно совместно использовать исходный код только для JVM и JS. В этом случае нужно найти отдельный набор исходного кода для этой пары целевых платформ, как описано в разделе Основы структуры многоплатформенного проекта.
Kotlin не создаёт такой набор исходного кода автоматически. Это значит, что его нужно создать вручную с помощью конструкции by creating:
kotlin {
jvm()
js()
linuxX64()
sourceSets {
// Create a source set named "jvmAndJs"
val jvmAndJsMain by creating {
// …
}
}
}
Однако Kotlin всё ещё не знает, как обрабатывать или компилировать этот набор исходного кода. Если изобразить структуру на диаграмме, этот набор исходного кода будет изолирован и не будет иметь меток целевых платформ:
Чтобы исправить это, включите jvmAndJsMain в иерархию, добавив несколько отношений dependsOn:
kotlin {
jvm()
js()
linuxX64()
sourceSets {
val jvmAndJsMain by creating {
// Don't forget to add dependsOn to commonMain
dependsOn(commonMain.get())
}
jvmMain {
dependsOn(jvmAndJsMain)
}
jsMain {
dependsOn(jvmAndJsMain)
}
}
}
Здесь jvmMain.dependsOn(jvmAndJsMain) добавляет целевую платформу JVM в jvmAndJsMain, а jsMain.dependsOn(jvmAndJsMain) добавляет целевую платформу JS в jvmAndJsMain.
Итоговая структура проекта будет выглядеть так:
Зависимости от других библиотек или проектов
В многоплатформенных проектах можно настроить обычные зависимости от опубликованной библиотеки или другого проекта Gradle.
В Kotlin Multiplatform зависимости обычно объявляются стандартным для Gradle способом. Как и в Gradle, вы:
Используете блок
dependencies {}в скрипте сборки.Выбираете подходящую область действия зависимости, например
implementationилиapi.Указываете зависимость либо по её координатам, если она опубликована в репозитории, например
"com.google.guava:guava:32.1.2-jre", либо по пути, если это проект Gradle в той же сборке, напримерproject(":utils:concurrency").
Настройка зависимостей в многоплатформенных проектах имеет некоторые особенности. У каждого набора исходного кода Kotlin есть собственный блок dependencies {}. Это позволяет объявлять платформенные зависимости в соответствующих платформенных наборах исходного кода:
kotlin {
// Targets declaration
sourceSets {
jvmMain.dependencies {
// This is jvmMain's dependencies, so it's OK to add a JVM-specific dependency
implementation("com.google.guava:guava:32.1.2-jre")
}
}
}
С общими зависимостями всё сложнее. Рассмотрим многоплатформенный проект, в котором объявлена зависимость от многоплатформенной библиотеки, например kotlinx.coroutines:
kotlin {
android() // Android
iosArm64() // iPhone devices
iosSimulatorArm64() // iPhone simulator on Apple Silicon Mac
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
}
При разрешении зависимостей важны три понятия:
-
Многоплатформенные зависимости распространяются вниз по структуре
dependsOn. Когда вы добавляете зависимость вcommonMain, она автоматически добавляется во все наборы исходного кода, для которых отношенияdependsOnпрямо или косвенно объявлены вcommonMain.В этом случае зависимость действительно автоматически добавлена во все наборы исходного кода
*Main:iosMain,jvmMain,iosSimulatorArm64MainиiosArm64Main. Все эти наборы исходного кода наследуют зависимостьkotlin-coroutines-coreот набора исходного кодаcommonMain, поэтому не нужно вручную копировать её в каждый из них: -
Зависимости набор исходного кода → многоплатформенная библиотека, например
commonMainотorg.jetbrians.kotlinx:kotlinx-coroutines-core:1.7.3выше, представляют промежуточное состояние разрешения зависимостей. Итоговое состояние разрешения всегда представлено зависимостями набор исходного кода → набор исходного кода.Чтобы определить детализированные зависимости набор исходного кода → набор исходного кода, Kotlin считывает структуру наборов исходного кода, публикуемую вместе с каждой многоплатформенной библиотекой. После этого шага каждая библиотека представляется внутри системы не как единое целое, а как набор её наборов исходного кода. См. этот пример для
kotlinx-coroutines-core: -
Kotlin разрешает каждое отношение зависимости в набор наборов исходного кода из зависимости. Каждый набор исходного кода зависимости в этом наборе должен иметь совместимые целевые платформы. Набор исходного кода зависимости имеет совместимые целевые платформы, если он компилируется как минимум для тех же целевых платформ, что и использующий его набор исходного кода.
Рассмотрим пример, в котором
commonMainв демонстрационном проекте компилируется дляandroid,iosArm64иiosSimulatorArm64:Сначала разрешается зависимость от
kotlinx-coroutines-core.commonMain. Это происходит потому, чтоkotlinx-coroutines-coreкомпилируется для всех возможных целевых платформ Kotlin. Поэтому егоcommonMainкомпилируется для всех возможных целевых платформ, включая требуемыеandroid,iosArm64иiosSimulatorArm64.Во-вторых,
commonMainзависит отkotlinx-coroutines-core.concurrentMain. ПосколькуconcurrentMainвkotlinx-coroutines-coreкомпилируется для всех целевых платформ, кроме JS, он соответствует целевым платформамcommonMainпотребительского проекта.
Однако такие наборы исходного кода, как
iosArm64Mainиз coroutines, несовместимы сcommonMainпотребителя. ХотяiosArm64Mainкомпилируется для одной из целевых платформcommonMain, а именноiosArm64, он не компилируется ни дляandroid, ни дляiosSimulatorArm64.Результаты разрешения зависимостей напрямую влияют на то, какой код из
kotlinx-coroutines-coreдоступен:
Согласование версий общих зависимостей между наборами исходного кода
В проектах Kotlin Multiplatform общий набор исходного кода компилируется несколько раз: для создания klib и в составе каждой настроенной компиляции. Чтобы получать согласованные бинарные файлы, общий код каждый раз должен компилироваться с одними и теми же версиями многоплатформенных зависимостей. Плагин Kotlin Gradle помогает согласовать эти зависимости и обеспечивает одинаковую эффективную версию зависимости для каждого набора исходного кода.
В примере выше представьте, что вы хотите добавить зависимость androidx.navigation:navigation-compose:2.7.7 в набор исходного кода androidMain. В проекте явно объявлена зависимость kotlinx-coroutines-core:1.7.3 для набора исходного кода commonMain, но библиотеке Compose Navigation версии 2.7.7 требуется Kotlin coroutines версии 1.8.0 или новее.
Поскольку commonMain и androidMain компилируются вместе, плагин Kotlin Gradle выбирает одну из двух версий библиотеки coroutines и применяет kotlinx-coroutines-core:1.8.0 к набору исходного кода commonMain. Но чтобы общий код согласованно компилировался для всех настроенных целевых платформ, наборы исходного кода iOS тоже должны использовать ту же версию зависимости. Поэтому Gradle распространяет зависимость kotlinx.coroutines-*:1.8.0 и на набор исходного кода iosMain.
Зависимости согласуются отдельно для наборов исходного кода *Main и *Test. Конфигурация Gradle для наборов исходного кода *Test включает все зависимости наборов исходного кода *Main, но не наоборот. Поэтому можно тестировать проект с более новыми версиями библиотек, не затрагивая основной код.
Например, в наборах исходного кода *Main используется зависимость от Kotlin coroutines версии 1.7.3, которая распространяется на каждый набор исходного кода проекта. Однако в наборе исходного кода iosTest вы решаете обновить версию до 1.8.0, чтобы протестировать новый выпуск библиотеки. Согласно тому же алгоритму эта зависимость распространится по всему дереву наборов исходного кода *Test, поэтому каждый набор исходного кода *Test будет компилироваться с зависимостью kotlinx.coroutines-*:1.8.0.
Компиляции
В отличие от одноплатформенных проектов, для сборки всех артефактов в проектах Kotlin Multiplatform требуется запускать компилятор несколько раз. Каждый запуск компилятора — это компиляция Kotlin.
Например, вот как в ходе упомянутой выше компиляции Kotlin создаются бинарные файлы для устройств iPhone:
Компиляции Kotlin объединяются в целевые платформы. По умолчанию Kotlin создаёт для каждой целевой платформы две компиляции: main для исходного кода продукта и test для тестового исходного кода.
В скриптах сборки доступ к компиляциям организован похожим образом. Сначала выбирается целевая платформа Kotlin, затем внутри неё открывается контейнер compilations и, наконец, по имени выбирается нужная компиляция:
kotlin {
// Declare and configure the JVM target
jvm {
val mainCompilation: KotlinJvmCompilation = compilations.getByName("main")
}
}
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform/multiplatform-advanced-project-structure.html