Основы структуры проекта Kotlin Multiplatform
С помощью Kotlin Multiplatform можно совместно использовать код на разных платформах. В этой статье объясняется, какие ограничения есть у общего кода, как отличить общие части кода от платформенно-зависимых и как указать платформы, на которых работает этот общий код.
Вы также узнаете об основных понятиях настройки проекта Kotlin Multiplatform: общем коде, целях компиляции, платформенно-зависимых и промежуточных наборах исходного кода, а также интеграции тестов. Это поможет вам в будущем настраивать многоплатформенные проекты.
Представленная здесь модель упрощена по сравнению с той, которую использует Kotlin. Однако этой базовой модели должно быть достаточно для большинства случаев.
Общий код
Общий код — это код на Kotlin, используемый совместно на разных платформах.
Рассмотрим простой пример «Hello, World»:
fun greeting() {
println("Hello, Kotlin Multiplatform!")
}
Код на Kotlin, используемый совместно на разных платформах, обычно находится в каталоге commonMain. Расположение файлов кода имеет значение, поскольку оно влияет на список платформ, для которых компилируется этот код.
Компилятор Kotlin получает исходный код на вход и создает в результате набор платформенно-зависимых бинарных файлов. При компиляции многоплатформенных проектов он может создать несколько бинарных файлов из одного и того же кода. Например, из одного файла Kotlin компилятор может создать файлы .class для JVM и исполняемые файлы для Native:
Не всякий фрагмент кода на Kotlin можно скомпилировать для всех платформ. Компилятор Kotlin не позволяет использовать в общем коде платформенно-зависимые функции или классы, если этот код невозможно скомпилировать для другой платформы.
Например, в общем коде нельзя использовать зависимость java.io.File. Она является частью JDK, тогда как общий код также компилируется в Native-код, где классы JDK недоступны:
В общем коде можно использовать библиотеки Kotlin Multiplatform. Эти библиотеки предоставляют общий API, который может реализовываться по-разному на разных платформах. В этом случае платформенно-зависимые API выступают в качестве дополнительных компонентов, а попытка использовать такой API в общем коде приводит к ошибке.
Например, kotlinx.coroutines — это библиотека Kotlin Multiplatform, поддерживающая все цели компиляции, но у нее также есть платформенно-зависимая часть, преобразующая примитивы конкурентного выполнения kotlinx.coroutines в примитивы конкурентного выполнения JDK, например fun CoroutinesDispatcher.asExecutor(): Executor. Эта дополнительная часть API недоступна в commonMain.
Список доступных библиотек Kotlin Multiplatform см. на сайте klibs.io.
Цели компиляции
Цели компиляции определяют платформы, для которых Kotlin компилирует общий код. Это могут быть, например, JVM, JS, Android, iOS или Linux. В предыдущем примере общий код компилировался для целей JVM и Native.
Цель компиляции Kotlin — это идентификатор, описывающий цель компиляции. Он определяет формат создаваемых бинарных файлов, доступные языковые конструкции и разрешенные зависимости.
Сначала необходимо объявить цель компиляции, чтобы указать Kotlin компилировать код для этой цели. В Gradle цели компиляции объявляются с помощью предопределенных вызовов DSL внутри блока kotlin {}:
kotlin {
jvm() // Declares a JVM target
iosArm64() // Declares a target that corresponds to 64-bit iPhones
}
Таким образом, каждый многоплатформенный проект определяет набор поддерживаемых целей компиляции. Подробнее об объявлении целей компиляции в скриптах сборки см. в разделе Иерархическая структура проекта.
После объявления целей jvm и iosArm64 общий код в commonMain будет скомпилирован для этих целей:
Чтобы понять, какой код будет скомпилирован для конкретной цели, можно представить цель компиляции как метку, присвоенную исходным файлам Kotlin. Kotlin использует эти метки, чтобы определить, как компилировать код, какие бинарные файлы создавать, а также какие языковые конструкции и зависимости разрешены в этом коде.
Если вы также хотите скомпилировать файл greeting.kt для .js, достаточно объявить цель JS. Тогда код в commonMain получит дополнительную метку js, соответствующую цели JS, которая указывает Kotlin создать файлы .js:
Так компилятор Kotlin работает с общим кодом, скомпилированным для всех объявленных целей. О том, как писать платформенно-зависимый код, см. в разделе Наборы исходного кода.
Наборы исходного кода
Набор исходного кода Kotlin — это набор исходных файлов со своими целями компиляции, зависимостями и параметрами компилятора. Это основной способ совместного использования кода в многоплатформенных проектах.
Каждый набор исходного кода в многоплатформенном проекте:
Имеет уникальное в рамках проекта имя.
Содержит набор исходных файлов и ресурсов, обычно хранящихся в каталоге с именем этого набора исходного кода.
Определяет набор целей компиляции, для которых компилируется код из этого набора исходного кода. Эти цели влияют на доступность языковых конструкций и зависимостей в наборе исходного кода.
Определяет собственные зависимости и параметры компилятора.
Kotlin предоставляет несколько предопределенных наборов исходного кода. Один из них — commonMain, который присутствует во всех многоплатформенных проектах и компилируется для всех объявленных целей.
В проектах Kotlin Multiplatform наборы исходного кода представлены каталогами внутри src. Например, проект с наборами исходного кода commonMain, iosMain и jvmMain имеет следующую структуру:
В скриптах Gradle доступ к наборам исходного кода осуществляется по имени внутри блока kotlin.sourceSets {}:
kotlin {
// Targets declaration:
// …
// Source set declaration:
sourceSets {
commonMain {
// Configure the commonMain source set
}
}
}
Помимо commonMain, остальные наборы исходного кода могут быть платформенно-зависимыми или промежуточными.
Платформенно-зависимые наборы исходного кода
Хотя использовать только общий код удобно, это не всегда возможно. Код в commonMain компилируется для всех объявленных целей, и Kotlin не позволяет использовать в нем платформенно-зависимые API.
В многоплатформенном проекте с целями Native и JS следующий код в commonMain не компилируется:
// commonMain/kotlin/common.kt
// Doesn't compile in common code
fun greeting() {
java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}
Для решения этой проблемы Kotlin создает платформенно-зависимые наборы исходного кода, которые также называются платформенными наборами исходного кода. Каждой цели компиляции соответствует платформенный набор исходного кода, который компилируется только для этой цели. Например, цели jvm соответствует набор исходного кода jvmMain, компилируемый только для JVM. Kotlin разрешает использовать в таких наборах исходного кода платформенно-зависимые зависимости, например JDK в jvmMain:
// jvmMain/kotlin/jvm.kt
// You can use Java dependencies in the `jvmMain` source set
fun jvmGreeting() {
java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}
Компиляция для определенной цели
Компиляция для определенной цели использует несколько наборов исходного кода. При компиляции многоплатформенного проекта для конкретной цели Kotlin собирает все наборы исходного кода с меткой этой цели и создает из них бинарные файлы.
Рассмотрим пример с целями jvm, iosArm64 и js. Kotlin создает набор исходного кода commonMain для общего кода и соответствующие наборы исходного кода jvmMain, iosArm64Main и jsMain для конкретных целей:
При компиляции для JVM Kotlin выбирает все наборы исходного кода с меткой «JVM», а именно jvmMain и commonMain. Затем они компилируются вместе в файлы классов JVM:
Поскольку Kotlin компилирует commonMain и jvmMain вместе, результирующие бинарные файлы содержат объявления как из commonMain, так и из jvmMain.
При работе с многоплатформенными проектами помните:
Чтобы Kotlin компилировал код для определенной платформы, объявите соответствующую цель компиляции.
-
Чтобы выбрать каталог или исходный файл для хранения кода, сначала решите, для каких целей компиляции вы хотите использовать этот код совместно:
Если код используется совместно для всех целей, его следует объявить в
commonMain.Если код используется только для одной цели, его следует определить в платформенно-зависимом наборе исходного кода для этой цели (например,
jvmMainдля JVM).
Код, написанный в платформенно-зависимых наборах исходного кода, может обращаться к объявлениям из общего набора исходного кода. Например, код в
jvmMainможет использовать код изcommonMain. Однако обратное неверно:commonMainне может использовать код изjvmMain.Код, написанный в платформенно-зависимых наборах исходного кода, может использовать соответствующие платформенные зависимости. Например, код в
jvmMainможет использовать библиотеки только для Java, такие как Guava или Spring.
Промежуточные наборы исходного кода
В простых многоплатформенных проектах обычно есть только общий и платформенно-зависимый код. Набор исходного кода commonMain представляет общий код, используемый для всех объявленных целей. Платформенно-зависимые наборы исходного кода, например jvmMain, представляют платформенно-зависимый код, компилируемый только для соответствующей цели.
На практике часто требуется более гибко делиться кодом.
Рассмотрим пример, в котором нужно поддерживать все современные устройства Apple и устройства Android:
kotlin {
android()
iosArm64() // 64-bit iPhone devices
macosArm64() // Modern Apple Silicon-based Macs
watchosArm64() // Modern 64-bit Apple Watch devices
tvosArm64() // Modern Apple TV devices
}
Также требуется набор исходного кода, чтобы добавить функцию генерации UUID для всех устройств Apple:
import platform.Foundation.NSUUID
fun randomUuidString(): String {
// You want to access Apple-specific APIs
return NSUUID().UUIDString()
}
Добавить эту функцию в commonMain нельзя. Набор commonMain компилируется для всех объявленных целей, включая Android, а platform.Foundation.NSUUID — это API только для Apple, недоступный на Android. При попытке обратиться к NSUUID в commonMain Kotlin покажет ошибку.
Можно скопировать и вставить этот код в каждый набор исходного кода для Apple: iosArm64Main, macosArm64Main, watchosArm64Main и tvosArm64Main. Однако такой подход не рекомендуется, поскольку дублирование кода может привести к ошибкам.
Решить эту проблему можно с помощью промежуточных наборов исходного кода. Промежуточный набор исходного кода Kotlin компилируется для некоторых, но не для всех целей проекта. Промежуточные наборы исходного кода также называют иерархическими наборами исходного кода или просто иерархиями.
Некоторые промежуточные наборы исходного кода Kotlin создает по умолчанию. В этом конкретном случае структура проекта будет выглядеть так:
Внизу находятся разноцветные блоки, представляющие платформенно-зависимые наборы исходного кода. Для наглядности метки целей компиляции не указаны.
Блок appleMain — это промежуточный набор исходного кода, созданный Kotlin для совместного использования кода, компилируемого для целей Apple. Набор исходного кода appleMain компилируется только для целей Apple. Поэтому Kotlin разрешает использовать API Apple в appleMain, и сюда можно добавить функцию randomUUID().
При компиляции для определенной цели Kotlin выбирает все наборы исходного кода с меткой этой цели, в том числе промежуточные. Поэтому при компиляции для платформенной цели iosArm64 весь код из наборов исходного кода commonMain, appleMain и iosArm64Main объединяется:
Цели для устройств Apple и симуляторов
При разработке мобильных приложений для iOS с помощью Kotlin Multiplatform обычно используется набор исходного кода iosMain. Может показаться, что это платформенно-зависимый набор исходного кода для цели ios, однако единой цели ios не существует. Для большинства мобильных проектов требуется как минимум две цели:
Цель для устройства используется для создания бинарных файлов, которые можно запускать на устройствах iOS. В настоящее время для iOS есть только одна цель для устройств:
iosArm64.Цель для симулятора используется для создания бинарных файлов для симулятора iOS, запущенного на вашем компьютере. Если у вас компьютер Mac с процессором Apple Silicon, выберите
iosSimulatorArm64в качестве цели для симулятора.
Если объявить только цель для устройства iosArm64, вы не сможете запускать и отлаживать приложение и тесты на локальном компьютере.
Платформенно-зависимые наборы исходного кода, например iosArm64Main и iosSimulatorArm64Main, обычно пусты, поскольку код Kotlin для устройств iOS и симуляторов обычно одинаков. Для совместного использования кода во всех этих наборах можно использовать только промежуточный набор исходного кода iosMain.
То же относится и к другим целям Apple, не предназначенным для Mac. Например, если у вас есть цель для устройства Apple TV tvosArm64 и цель для симулятора Apple TV на устройствах с Apple Silicon tvosSimulatorArm64, для всех них можно использовать промежуточный набор исходного кода tvosMain.
Интеграция с тестами
В реальных проектах наряду с основным рабочим кодом нужны тесты. Поэтому у всех наборов исходного кода, создаваемых по умолчанию, есть суффиксы Main и Test. Main содержит рабочий код, а Test — тесты для этого кода. Связь между ними устанавливается автоматически, и тесты могут использовать API, предоставляемый кодом Main, без дополнительной настройки.
Наборы Test также являются аналогами наборов исходного кода Main. Например, commonTest соответствует commonMain и компилируется для всех объявленных целей, позволяя писать общие тесты. Платформенно-зависимые наборы исходного кода для тестов, такие как jvmTest, используются для написания платформенно-зависимых тестов, например тестов только для JVM или тестов, которым нужны API JVM.
Помимо набора исходного кода для общих тестов, вам также нужна многоплатформенная инфраструктура тестирования. В Kotlin по умолчанию предоставляется библиотека kotlin.test, включающая аннотацию @kotlin.Test и различные методы проверки утверждений, например assertEquals и assertTrue.
Платформенно-зависимые тесты можно писать как обычные тесты для каждой платформы в соответствующих наборах исходного кода. Как и для основного кода, для каждого набора исходного кода можно использовать платформенно-зависимые зависимости, например JUnit для JVM и XCTest для iOS. Чтобы запустить тесты для определенной цели, используйте задачу <targetName>Test.
О том, как создавать и запускать многоплатформенные тесты, читайте в руководстве Тестирование многоплатформенного приложения.
Что дальше?
© 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-discover-project.html